Skip to content

Print Formats with Jinja

Ronaldo edited this page Aug 5, 2020 · 16 revisions

The idea of this application is to prevent the problems created by pre-printed standard forms, where one is legally obligated to print the invoice data.

Customers will want specific formatting anyway, so some previous work we had done is relevant to this topic.

Jinja is a templating engine. It allows users to format data with full unicode support, with Python, CSS and HTML.

Python Logo HTML Logo CSS Logo Jinja Logo

In ERPNext, the user can define print templates in HTML and CSS with Jinja templating for Server-side rendering. The HTML allows the user control of which kind of elements appear on a printable page. CSS helps define styles for wide usage in the HTML page that will be rendered. This example / tutorial will let you merge all the necessary data on the page leveraging the power of Jinja by enabling the insertion of previously defined variables, whose specific values are called when rendering the document to serve it or output. The beauty of it is that it is easily mixed with an HTML (or other) document.

In our particular case having Jinja available in a print format allows us to pull data from the DocType being printed, and beyond.

Synopsis Squared²

The original synopsis is here Jinja depends on four main components. We will be generally using the first three.

  • Statements {% ... %}
  • Expressions {{ ... }}
  • Comments {# ... #}
  • Line Statements # ... ##

Anatomy of a Jinja Template

This document explores the basics of a Jinja Template for a specific application, and those items that really helped us make it work. It was only through several iterations that we finally got it working. I hope this is a good base for your template that can help you grab those elements which will serve you for your particular application.

Region and Language

One of the most frustrating aspects when configuring your template for use with ERPNext will be the region and language that you work with. In our case, our format was destined for printing in spanish, and thus it was important to get the language right. The specific problem that frustrated us perniciously was the "amount in words" which is a frappe function depending on the num2words library.

Header

The header of our template code is commented like this, to alert whoever edits the template about important aspects of its modification. We are alerting the user to configure the System language to be: es We found that using the required regional language es_GT caused a misprint in the amount in words, given that this module had not been added to the num2words python library. We have since modified it and submitted a Pull request, but the quick solution was to default to a language which could print the amount in words properly.

<!-- ## WARNING: To prevent printing errors: The System language must be: es ## -->
<!-- ##  You can configure this in Setup> System Settings > Language ## -->
<!-- ##  Esto se configura en Configuracion> Ajustes del Sistema > Idioma ## -->

Define the Style

Next up will be our HTML tag for style. Whatever is in this section will be within these tags, until we begin with the first <div> tag.

<style></style>

Media screen format

First up is the style that will define the size of the screen visible to the user from within ERPNext. This setting also affects the size of the document that will be configured in your printer, so it is very important that this is set to the width and height (if necessary) of the document you wish to print. Note: I use these comment lines to make it clearly evident in the Jinja file what it is I need to edit. Once I am done setting it up, I remove extra characters. I also leave comments with the measured amounts above the actual settings for the CSS. This helps to remember the measured size in case you need to adjust by one or two millimeters the final viewport sizes to get an accurately sized document print.

/* ################################ 1.1 Size of the viewport ############################ */
@media screen {
.print-format {
/*PRINTABLE WIDTH: 21.59cm, 8.5in */
width: 21.59cm;
/*PRINTABLE HEIGHT: 27.94cm, 11.00in */
min-height: 27.94cm;
/*PADDING FROM MARGINS: 0.9cm, .354331in */
padding: 0.0cm;
}
}

Position adjustment settings

This section I added to save me time in the future when customers tell you something like: "can we move everything to the right/ left or further down/up?"

These value smust equal the measured height and width of the document.

/* Only modify the variable numbers and not the names */
/* Width and Height in Centimeters 21.59 o 27.94 */
{% set alto_doc_cm = 27.94 %} /* 27.94 */
{% set ancho_doc_cm = 21.59 %} /* 21.59 */

This is where you adjust the location of every element on the page. Every element's original position can be corrected by altering the data on one line.

/* Error adjustment to move everything by a specified quantity. */
{% set error_top_cm = 0 %} /* -1.5 A negative value here moves everything up */
{% set error_left_cm = 0 %} /* -1.88 A negative value here moves everything to the left */

This sets the margin of the page in centimeters

/* Left, right, top and bottom margins in centimeters */
{% set margin_left_cm = 0.74 %}
{% set margin_right_cm = 0.74 %}
{% set margin_top_cm = 1.91 %}
{% set margin_bottom_cm = 1.91 %}

This sets the width of the table which will contain the individual items listed on the Sales invoice.

/* The width of the table is calculated automatically */
{% set item_table_width = ancho_doc_cm-(margin_left_cm+margin_right_cm) %}

CSS styles that take document dimension variables

These styles define the location of the main divs on the page. One is a "master" or "general" container div that sets the start at coordinates 0,0 in an x, y plane.

div.general{
position: relative;
top: 0.0cm;
/*left: 0.0cm; */
}

This specific div manages the document size, taking values from the variables declared above, and compiling the CSS element that defines it.

div.tamano_documento{
position: absolute;
height: {{alto_doc_cm}}cm;
width: {{ancho_doc_cm}}cm;
}

Document styles

These styles define the text color, size, font, etc. for the entire document.

.print-format div, .print-format p, .print-format span, .print-format tr, .print-format td {
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
line-height: 0%;
vertical-align: middle;
}

Clean up the Naming series

In our case, our naming series contains a period "." in ERPNext, so we assign the value to a variable which we clean with Python's replace method. Notice that here we use the Jinja Statement methodology as shown above using {% %} to enclose the python variable assignment or other statement.

/* In this case we remove any "periods" in the series and assign it to the variable that we will print. */
{% set original_series = doc.naming_series %}
{% set clean_series = original_series.replace(".", "") %}

Obtain data from other DocTypes

We gather specific data from other DocTypes, such as your company name, address, custom DocType fields, and other required data. This needs to be adjusted to what you require for your print format.

{% set company_fields = frappe.get_doc('Company',doc.company) %}
{% set address_fields = frappe.get_doc('Address','Sala de Ventas-Billing') %}
{% set config_series_fields = frappe.get_doc('Configuracion Series','1cc49e63f6') %} /* FIXME */
{% set config_facelec = frappe.get_doc('Configuracion Factura Electronica','CONFIG-FAC00001') %}

Text variables

I set these variables because I wanted to just scroll down 100 lines instead of 400 lines where I was gong to be using them. This an example of how you can declare variables in Jinja to be used within a document. Clearly, it is redundant because you can change the actual HTML instead, but this helped me change the values faster.

{% set firma_digital = 'FIRMA DIGITAL: ' %}
{% set dte_part1 = 'Documento Tributario Electrónico Según Resolución SAT:' %}
{% set dte_part2 = ' De fecha: ' %}
{% set dte_part3 = ' Serie: ' %}
{% set dte_part4 = ' Del ' %}
{% set dte_part5 = ' Al ' %}
{% set dte_part6 = ' GFACE: ' %}
{% set dte_part7 = ' NIT: ' %}

Logo Styles

Usually, the invoice will have the company logo printed on the header. Here I indicate the position on the document. Please note that images will be drawn to the right and down from this point. I also declare the path, the Alternate text, the size in pixels and the style for the alternate text, in case it shoul dbe printed. Again because of convenience when modifying the document, so I do not have to scroll down too much.

TIP: If a CSS style is not loading properly, try appending the text !important! right after the parameter you want to ensure that it works.

/* Indicate the desired position of the logo in cm from the top and left of origin 0,0. */
/* The images are going to be drawn to the right and down from this point. */
{% set top_logo = 0.3 %}
{% set left_logo = 0.74 %}

/* Indicate the path to the image */
{% set logo_path_to_image = "/files/example-factura-gr.jpg" %}

/* Indicat the Alternate text for the image (in case the link is not loaded, this text will be displayed */
{% set logo_alt_text = "◎ Company logo alternate" %}

/* Size of the image in pixels*/

{% set width_logo = 195.19 %}
{% set height_logo = 69.09 %}

/* Fast adjustment of the image scaling, as a percentage value. From 0 and beyond. */
{% set logo_scaling = 100 %}
{% set scaled_width_logo = width_logo * (logo_scaling / 100) %}
{% set scaled_height_logo = height_logo * (logo_scaling / 100) %}

/* Declare the CSS style, taking the values for width and height */
img.logo{
width: {{ scaled_width_logo }}px;
height: {{ scaled_height_logo }}px;


/* Indicate the style of text, color, size, and others just for the Alternate text */
/* This ensures that in case the logo does not load, the alternate text, has the desired format. */
font-size: 10pt;
font-weight: 700;
color: #99989a !important;
}

Note the Important! tag for the color of the alternate text in the CSS declarationright above. This helps in cases where the style will not load for some reason. (And saves a lot of frustration)

Title styles

Now we define the styles for the Title on the invoice. As before, we declare the starting position, the style, and then the correction block. The correction block will be present after each element's style. I opted to have control over each individual element from the start, and clearly I could manage general styles to simplify, but in this ocassion, I like to have control over each individual element. Hence the long style declaration.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_titulo = 1.0 %}
{% set left_titulo = 14.32 %}

/* Indicate the seller company Title style for text, color, size and other parameters */
p.doc-titulo{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_titulo = top_titulo + error_top_cm %}
{% set corrected_left_titulo = left_titulo + error_left_cm %}
span.doc-titulo{
position: absolute;
top: {{ corrected_top_titulo }}cm;
left: {{ corrected_left_titulo }}cm;
}

Invoice series label style

This block is similar to the title style, but for the invoice naming series label.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_serie = 2.00 %}
{% set left_serie = 14.32 %}

/* Indicate the seller company series label style for text, color, size and other parameters */
p.doc-serie{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_serie = top_serie + error_top_cm %}
{% set corrected_left_serie = left_serie + error_left_cm %}
span.doc-serie{
position: absolute;
top: {{ corrected_top_serie }}cm;
left: {{ corrected_left_serie }}cm;
}

Invoice numbering series style

This block is similar to the above block, but for the invoice naming series numbers.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_corr = 2.5 %}
{% set left_corr = 14.32 %}

/* Indicate the seller company numbering series style for text, color, size and other parameters */
p.doc-correlativo{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_corr = top_corr + error_top_cm %}
{% set corrected_left_corr = left_corr + error_left_cm %}
span.doc-correlativo{
position: absolute;
top: {{corrected_top_corr}}cm;
left: {{corrected_left_corr}}cm;
}

Seller Company Address

For this invoice, the address is divided into three separate lines. So we have declared style for each. You could theoretically declare a single style for the address font size, weight and color. I just leave this for control when designing the invoice.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_company_address_1 = 2.90 %}
{% set left_company_address_1 = 0.8 %}
{% set top_company_address_2 = 3.25 %}
{% set left_company_address_2 = 0.8 %}
{% set top_company_address_3 = 3.65 %}
{% set left_company_address_3 = 0.8 %}

/* Indicate the seller company address style for text, color, size and other parameters */
p.address1{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 9pt;
font-weight: 200;
color: #99989a !important;
}
p.address2{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 9pt;
font-weight: 200;
color: #99989a !important;
}
p.address3{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 9pt;
font-weight: 200;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_company_address_1 = top_company_address_1 + error_top_cm %}
{% set corrected_left_company_address_1 = left_company_address_1 + error_left_cm %}
span.address1{
position: absolute;
top: {{ corrected_top_company_address_1 }}cm;
left: {{ corrected_left_company_address_1 }}cm;
}
{% set corrected_top_company_address_2 = top_company_address_2 + error_top_cm %}
{% set corrected_left_company_address_2 = left_company_address_2 + error_left_cm %}
span.address2{
position: absolute;
top: {{ corrected_top_company_address_2 }}cm;
left: {{ corrected_left_company_address_2 }}cm;
}
{% set corrected_top_company_address_3 = top_company_address_3 + error_top_cm %}
{% set corrected_left_company_address_3 = left_company_address_3 + error_left_cm %}
span.address3{
position: absolute;
top: {{ corrected_top_company_address_3 }}cm;
left: {{ corrected_left_company_address_3 }}cm;
}

Seller company e-mail styles

Same pattern, now for the e-mail of the seller company (your company))

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_company_email = 4.25 %}
{% set left_company_email = 0.8 %}

/* Indicate the seller company e-mail style for text, color, size and other parameters */
p.company_email{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 9pt;
font-weight: 600;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_company_email = top_company_email + error_top_cm %}
{% set corrected_left_company_email = left_company_email + error_left_cm %}
span.company_email{
position: absolute;
top: {{ corrected_top_company_email }}cm;
left: {{ corrected_left_company_email }}cm;
}

Seller company name

The seller company name has to be printed on the invoice, so here we define the style for that.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_doc_nombre_empresa = 4.97 %}
{% set left_doc_nombre_empresa = 0.8 %}

/* Indicate the seller company name style for text, color, size and other parameters */
p.doc-nombre-empresa{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 200;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_doc_nombre_empresa = top_doc_nombre_empresa + error_top_cm %}
{% set corrected_left_doc_nombre_empresa = left_doc_nombre_empresa + error_left_cm %}
span.doc-nombre-empresa{
position: absolute;
top: {{corrected_top_doc_nombre_empresa}}cm;
left: {{corrected_left_doc_nombre_empresa}}cm;
width: {{ corr_item_table_width }}cm !important;
}

Seller company Tax Id

The seller company Taxpayer Identification Number has to be printed on the invoice, so here we define the style for that.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_doc_tax_id_empresa = 5.37 %}
{% set left_doc_tax_id_empresa = 0.8 %}

/* Indicate the seller company tax id style for text, color, size and other parameters */
p.doc-tax-id-empresa{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 200;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_doc_tax_id_empresa = top_doc_tax_id_empresa + error_top_cm %}
{% set corrected_left_doc_tax_id_empresa = left_doc_tax_id_empresa + error_left_cm %}
span.doc-tax-id-empresa{
position: absolute;
top: {{corrected_top_doc_tax_id_empresa}}cm;
left: {{corrected_left_doc_tax_id_empresa}}cm;
}

Issue Date styles

The date of issuance for the invoice is required, and the styles are declared here.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_fecha_doc = 6.50 %}
{% set left_fecha_doc = 0.8 %}

/* Indicate the issue date style for text, color, size and other parameters */
p.fecha-documento{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 11pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_fecha_doc = top_fecha_doc + error_top_cm %}
{% set corrected_left_fecha_doc = left_fecha_doc + error_left_cm %}
span.fecha-documento{
position: absolute;
top: {{corrected_top_fecha_doc}}cm;
left: {{corrected_left_fecha_doc}}cm;
width: {{ corr_item_table_width }}cm !important;
}

Customer Name Styles

The customer name from ERPNext for the invoice is required, and the styles are declared here.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_razon_social = 7.56 %}
{% set left_razon_social = 0.8 %}

/* Indicate the seller customer name style for text, color, size and other parameters */
p.razon-social{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 11pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_razon_social = top_razon_social + error_top_cm %}
{% set corrected_left_razon_social = left_razon_social + error_left_cm %}
span.razon-social{
position: absolute;
top: {{corrected_top_razon_social}}cm;
left: {{corrected_left_razon_social}}cm;
width: {{ corr_item_table_width }}cm !important;
}

Customer Address Styles

The customer address styles from ERPNext are declared here.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_direccion = 8.36 %}
{% set left_direccion = 0.8 %}

/* Indicate the customer address style for text, color, size and other parameters */
p.direccion{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 11pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_direccion = top_direccion + error_top_cm %}
{% set corrected_left_direccion = left_direccion + error_left_cm %}
span.direccion{
position: absolute;
top: {{corrected_top_direccion}}cm;
left: {{corrected_left_direccion}}cm;
width: {{ corr_item_table_width }}cm !important;
}

Taxpayer ID Styles

The customer taxpayer ID from ERPNext is legally requred to be shown on the invoice, so styles are declared here.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_nit = 9.16 %}
{% set left_nit = 0.8 %}

/* Indicate the customer address style for text, color, size and other parameters */
p.nit{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_nit = top_nit + error_top_cm %}
{% set corrected_left_nit = left_nit + error_left_cm %}
span.nit{
position: absolute;
top: {{corrected_top_nit}}cm;
left: {{corrected_left_nit}}cm;
width: {{ corr_item_table_width }}cm !important;
}

"Top Block" of the Invoice

OK. At this point we have defined what I like to call the "Top Block" of the invoice, which contains elements defining the invoice. Next, we will declare the styles for the items. I have found that using an HTML table is most effective for this. But, care must be taken to properly define the styles for the elements in the table so that they are shown accurately.

Alignment values of text within the cells

FIXME: There is a particular way on declaring table headers on a table, and I still must implement the HTML declaration accordingly.

/* Indicate the starting position of the table in cm from top and left */
{% set top_items = 9.85 %}
{% set left_items = 0.8 %}

/* Indicate the height of the header for the table in pixels */
{% set height_encabezado = 22 %}

/* Indicate the vertical alignment of the text in the header of the table in pixels */ FIXME!
/* vertical-align: Working OK the use of a string of text alignment */
{% set vert_align_encabezado = "middle" %}

/* Indicate where you align the quantity horizontally within the cell in the item table */
/*  "left", "right", "center", "justify", "char" */
{% set cantidad_align = "left" %}

/* Indicate where you align the description horizontally within the cell in the item table */
/*  "left", "right", "center", "justify", "char" */
{% set descripcion_align = "left" %}

/* Indicate where you align the unit price horizontally within the cell in the item table */
/*  "left", "right", "center", "justify", "char" */
{% set precio_unitario_align = "left" %}

/* Indicate where you align the subtotal amount horizontally within the cell in the item table */
/*  "left", "right", "center", "justify", "char" */
{% set valor_subtotal_align = "left" %}

Master alignment of all the items

This is the place where you will spend most of the time arranging items. Several trial and error prints will be necessary to adjust. However, starting with a solid variable declaration and printing on a scanned image of your format will help you more quickly align and move out of the print format configuration phase.

In this particular case, I found it effective to declare two different tables, one for the items, and one for the footer, because it was easier to align items below without affecting how the items above will display.

span.items{
position: absolute;
top: {{ corrected_top_items }}cm;
left: {{ corrected_left_items }}cm;
width:
}

table.items-example{
width: {{ corr_item_table_width }}cm;
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
/*height: 1cm;*/
height: 16.00cm;
border-spacing: 0;
padding: 1px;
border-collapse: collapse;
border: 1px;
text-color: #99989a !important;
font-size: 10pt;
font-weight: 700;
font-color: #99989a !important;
border-style: solid;
border-color: #99989a !important;
}

table.items-example-footer{
width: {{ corr_item_table_width }}cm;
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
/*height: 1cm;*/
height: 1.0cm;
border-spacing: 0;
padding: 1px;
border-collapse: collapse;
border: 1px;
text-color: #99989a !important;
font-size: 10pt;
font-weight: 700;
font-color: #99989a !important;
border-style: solid;
border-color: #99989a !important;
}

Notice how we have to use in several CSS delcarations the !Important modifier to ensure these elements are displayed correctly.

Table style declarations

Now that we have the variables and the table styles, we can specify the individual values for specific table elements. Most important here is the declaration of widths, using a percentage value. This will be a trial and error value change, but we have found them to be effective.

td.cantidad-header {
width: 8%
padding: 9px;
background-color: #99989a !important;
height: {{ height_encabezado }}px;
text-align: {{ valor_cantidad_align }};
}
p.cantidad-header{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #FFFFFF !important;
}
td.cantidad{
width: 8%
padding: 0px;
/*align: left|right|center|justify|char */
text-align: {{ cantidad_align }};
}
p.cantidad{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 600;
color: #99989a !important;
}
td.descripcion-header{
width: 56%
padding: 9px;
background-color: #99989a !important;
height: {{ height_encabezado }}px;
text-align: {{ valor_descripcion_align }};
}
p.descripcion-header{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 600;
color: #FFFFFF !important;
}
td.descripcion{
width: 56%
padding:0px;
/*align: left|right|center|justify|char */
text-align: {{ descripcion_align }};
}
p.descripcion{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 600;
color: #99989a !important;
}
td.precio_unitario-header {
width: 17%
padding: 9px;
background-color: #99989a !important;
height: {{ height_encabezado }}px;
text-align: {{ valor_precio_unitario_align }};
}
p.precio_unitario-header{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #FFFFFF !important;
}
td.precio_unitario{
width: 17%
padding:0px;
text-align: {{ precio_unitario_align }};
}
p.precio_unitario{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 600;
color: #99989a !important;
}
td.valor_subtotal-header {
width: 17%
padding: 9px;
background-color: #99989a !important;
height: {{ height_encabezado }}px;
text-align: {{ valor_subtotal_align }};
}
p.valor_subtotal-header {
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #FFFFFF !important;
}
td.valor_subtotal {
width: 17%
padding:0px;
text-align: {{ valor_subtotal_align }};
}
p.valor_subtotal {
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 600;
color: #99989a !important;
}
td.total-en-letras {
width: 71%
padding:0px;
text-align: {{ valor_subtotal_align }};
}
td.total {
width: 29%
padding:0px;
text-align: {{ valor_subtotal_align }};
}

Table footer style declarations

We finally declare the styles for all the footer elements of the table, such as Total in Words, Total, Digital signature and Fiscally required elements. The important aspect here, particularly in the context of the application for which this file is hosted, is the electronic invoice digital signature aka CAE.

Total Value styles and position

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_total = 26.30 %}
{% set left_total = 15.28 %}

/* Indicate the item total style for text, color, size and other parameters */
p.total{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 10pt;
font-weight: 700;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_total = top_total + error_top_cm %}
{% set corrected_left_total = left_total + error_left_cm %}
span.total{
position: absolute;
top: {{corrected_top_total}}cm;
left: {{corrected_left_total}}cm;
}

Digital Signature (CAE) styles and position

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_cae = 27.00 %}
{% set left_cae = 0.8 %}

/* Indicate the CAE style for text, color, size and other parameters */
p.cae{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 9pt;
font-weight: 400;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_cae = top_cae + error_top_cm %}
{% set corrected_left_cae = left_cae + error_left_cm %}
span.cae{
position: absolute;
top: {{corrected_top_cae}}cm;
left: {{corrected_left_cae}}cm;
}

Fiscal entity resolution (authorization numbers) and other items

The fiscal entity requires the printing of the officially furnished resolution or authorization to make invoices. This helps with their audit process, and thus mus tbe printed on the invoices. Penalties might ensue if this is not taken into account.

/* Indicate the position in cm from the top and left of origin: 0,0 */
{% set top_resolucion = 27.30 %}
{% set left_resolucion = 0.8 %}

/* Indicate the Resolution style for text, color, size and other parameters */
p.resolucion{
font-family: avenir, 'avenir next', helvetica, arial, sans-serif;
font-size: 8pt;
font-weight: 200;
color: #99989a !important;
}

/* DO NOT MODIFY THIS */
{% set corrected_top_resolucion = top_resolucion + error_top_cm %}
{% set corrected_left_resolucion = left_resolucion + error_left_cm %}
span.resolucion{
position: absolute;
top: {{ corrected_top_resolucion }}cm;
left: {{ corrected_left_resolucion }}cm;
}

Closing the style tag

With this, we conclude the style declarations, and for this example it is about 705 lines. You could define this in a separate file, and link it to this HTML, one trick would be to place the CSS file to be linked within the private folder in ERPNext, and link to it. I opted to define it here because I do not like to depend much on other files, since the CSS file could be deleted accidentally by another user with similar privileges.

</style>

HTML

Now we finally begin with the HTML declaration to fill the document with the actual elements by drawing them on the viewport This creates the actual Sales Invoice!

HTML Begins and first <div> alignment.

This element is the empty container within which we will place all the other items. This is the initial alignment element. Everything is defined in terms of this div element and aligns everything to the origin point 0,0

<!-- 2. HTML Begins -->
<!--  2.1 First div, container begins -->
<div class="general">
<!--  2.1 First div, container ends -->

Document size

We now define the size of the document, taking all the size variables declared in the CSS class named above.

<!-- 2.2 Document Size Begins -->
<!-- This div defines the size of the entire document. This "expands" the document to fill the viewport size also declared above.-->
    <div class="tamano_documento">
<!-- 2.2 Document Size Ends -->

Logo placement

We place the logo. Instead of a div we use a spanelement. Here we utilize specific variables by printing them with Jinja using the expression methodology with {{ and }}

<!-- 2.3 Logo Begins -->
span class="doc-logo">
<img class="logo" src="{{ logo_path_to_image }}" alt="{{logo_alt_text }}">
</span>
<!-- 2.3 Logo Ends -->

Title placement

We place the title. Again, with a Jinja declaration {{ }}. Notice that we are enclossing a Frappe method within the declaration, with eventual aim to have this translated.

<!-- 2.4 Title Begins -->
<span class="doc-titulo">
<p class="doc-titulo">{{ _("FACTURA") }}</p>
</span>
<!-- 2.4 Title Ends -->

Naming Series placement

We place the Title of the numbering series. Again, with a Jinja declaration {{ }}. We use the clean_series variable after removing the period with a Jinja Statement {% %}.

<!-- 2.5 Naming Series Begins -->
<span class="doc-serie">
<p class="doc-serie">{{ _("SERIE") }}: {{ clean_series }}
</p>
</span>
<!-- 2.5 Naming Series Ends -->

Numbering Series placement

We place the actual numbering series. Again, with a Jinja declaration preceded by a translatable text (for the future) {{ }}.

<!-- 2.6 Title Begins -->
<span class="doc-correlativo">
<p class="doc-correlativo">{{ _("No.") }}: {{ doc.name }}</p>
</span>
<!-- 2.6 Title Ends -->

Invoicing company address

We place the three lines for the address. The styles help change each line style, but the data actually included is different and modifiable. In this case we use one line for the actual address, another for the secondary location data, country (changing it all to uppercase with Pythons upper() method, followed by city, and postal code. Then the final line holds the telephone number, which is not required but rather requested by some customers.

<!-- 2.7 Invoicing company address Begins -->
<span class="address1">
<p class="address1">{{ address_fields.address_line1 }}, </p>
</span>
<span class="address2">
<p class="address2">{{ address_fields.address_line2 }} {{ address_fields.country.upper() }}, {{ address_fields.city }}, C.A., {{ address_fields.pincode}}</p>
</span>
<span class="address3">
<p class="address3">TELÉFONO: {{ address_fields.phone }}</p>
</span>
<!-- 2.7 Invoicing company address Ends -->

Email of invoicing company

We now place the email address, as requested by the customer for this example.

<!-- 2.8 Invoicing company email Begins -->
<span class="company_email">
<p class="company_email">{{ address_fields.email_id}} /</p>
</span>
<!-- 2.8 Invoicing company email Ends -->

Website of invoicing company

Another requirement of our customer is to have the website listed.

<!-- 2.9 Invoicing company website Begins -->
<span class="company_website">
<p class="company_website">{{ company_fields.website}}</p>
</span>
<!-- 2.9 Invoicing company website Ends -->

Invoicing company name

The name

<!-- 3.0 Invoicing company name Begins -->
<span class="doc-nombre-empresa">
<p class="doc-nombre-empresa">{{ doc.company }}</p>
</span>
<!-- 3.0 Invoicing company name Ends -->

Invoicing company Tax Id

A requirement for invoices issued in Guatemala, the issuing company must print its tax ID

<!-- 3.1 Invoicing company Tax ID Begins -->
<span class="doc-tax-id-empresa">
<p class="doc-tax-id-empresa">NIT: {{ company_fields.nit_face_company}}</p>
</span>
<!-- 3.1 Invoicing company Tax ID Ends -->

Document Date

I used this reference: Python Formatting a date in Jinja, however frappe has its own date formatting method which we use here.

<!-- 3.1 Posting Date Begins -->
<span class="fecha-documento">
<p class="fecha-documento">LUGAR Y FECHA: {{ address_fields.city }}, {{ doc.get_formatted("posting_date") }}</p>
</span>
<!-- 3.1 Posting Date Ends -->

Customer Name, Address and Taxpayer ID

Here we print the customer name, billing address and Taxpayer ID. It is important that you configure the Customers address with the check for "Is Billing Address", otherwise it will not appear here.

<!-- 3.2 Customer name Begins -->
<span class="razon-social">
<p class="razon-social">NOMBRE: {{ doc.customer_name }}</p>
</span>
<!-- 3.2 Customer name Ends -->

<!-- 3.3 Customer address Begins -->
<span class="direccion">
<p class="direccion">DIRECCIÓN: {{ doc.billing_address }}</p>
</span>
<!-- 3.3 Customer address Ends -->

<!-- 3.4 Customer Taxpayer ID Begins -->
<span class="nit">
<p class="nit">NIT: {{ frappe.db.get_value("Customer", doc.customer, "tax_id") }}</p>
</span>
<!-- 3.4 Customer Taxpayer ID Ends -->

Item Table

This is where most of the magic happens with Jinja, and where its power allows you to print lines conditionally using a for loop.

<!-- 3.5 Item Table Begins -->
<span class="articulos">
    <table class="items-example">
        <tbody>
            <tr>
                <td class="cantidad-header"><p class="descripcion-header">CANTIDAD</p></td>
                <td class="descripcion-header"><p class="descripcion-header">DESCRIPCIÓN</p></td>
                <td class="precio_unitario-header"><p class="precio_unitario-header">PRECIO UNITARIO</p></td>
                <td class="valor_subtotal-header"><p class="valor_subtotal-header">VALOR</p></td>
            </tr>
            {%- for item in doc.items  -%}
            <tr>
                <td class="cantidad"><p class="cantidad">{{ item.qty }}</p></td>
                <td class="descripcion">
                <p class="descripcion">
                {%- if item.item_name != item.item_code -%}
                {{ item.item_name }}{%- endif -%}
                </p>
                </td>
                <td class="precio_unitario"><p class="precio_unitario">{{ item.get_formatted("rate") }}</p></td>
                <td class="valor_subtotal"><p class="valor_subtotal">{{ item.get_formatted("amount") }}</p></td>
            </tr>
                {%- endfor -%}
        </tbody>
    </table>
    <table class="items-example-footer">
        <tbody>
            <tr><td></td></tr>
        </tbody>
    </table>
</span>
<!-- 3.5 Item Table Ends -->

Total in Words section

This section encases all the items, and it is the footer of the table.

<!-- 3.6 Total in Words Begins -->
<span class="total-en-letras">
    <!-- ### We opted to cut the first three letters of the format that ERPNext provides for monetary amounts (Three letter code) for this customer, since they will not be accepting any payments in other currencies. ## -->
    <!-- ### It is not affected the Global default of "Hide Currency Symbol" / "Ocultar el símbolo de moneda"## , this is why we used it. -->
    {% set in_words_without_first_3 = doc.get_formatted("base_in_words") %}
    {% set en_letras_length = in_words_without_first_3|length %}
    {% set en_letras_limpio = in_words_without_first_3[3:en_letras_length] %}
    <p class="total-en-letras">CANTIDAD EN LETRAS: {{ en_letras_limpio }}</p>
</span>
<!-- 3.6 Total in Words Ends -->

Grand total

This is the grand total for the invoice in numbers.

<!-- 3.7 Grand Total Begins -->
<span class="total">
<p class="total">TOTAL {{ doc.get_formatted("grand_total") }}</p>
</span>
<!-- 3.7 Grand Total Ends -->

Digital Signature and other Fiscally required data

The digital signature for invoices which were validated wsith the fiscal authority through a paid service, a requirement for printing on plain paper.

<!-- 3.8 Digital Signature Begins -->
<span class="cae">
<p class="cae">{%- if doc.cae_factura_electronica != None -%}
{{ firma_digital }} {{ doc.cae_factura_electronica }}{%- endif -%}</p>
</span>
<!-- 3.8 Digital Signature Ends -->

<!-- 3.9 Fiscally required data Begins -->
<span class="resolucion">
<p class="resolucion">{{ dte_part1 }}{{ config_series_fields.numero_resolucion }}{{ dte_part2 }}{{ config_series_fields.fecha_resolucion }}{{ dte_part3 }}{{ config_series_fields.secuencia_infile }}{{ dte_part4 }} 1 {{ dte_part5 }} 1000000 {{ dte_part6 }} INFILE {{ dte_part7 }} {{ config_facelec.nit_gface}}</p>
</span>
<!-- 3.9 Fiscally required data Ends -->

Closing all the remaining HTML elements

Finally, close the table </div> and the main container </div> for the document.

</div>
</div>

Conclusion

So far, this document has covered some of the most essential elements for a legally compliant invoice. It is clearly not trivial, and the amount of code written is a lot. This is my extended initial code, and it surely can be shortened and made more concise. I opted to share this version because it shows the possibility of changing every detail you desired.

Benefits of these methods

  1. You can create a print format that will allow printing on an existing invoice format. If you measure the existing page and the element positions and transfer those measurements here, you will be able to print them accurately.
  2. Despite the lenghiness, and not being a best practice, it is an effective and highly controllable methodology. Use this only as a base to create your own Jinja print formats. It is by no means a definitive method, but rather a proposal of a practice that gave us excellent results.

Tips for usage of these methods

  1. Feel free to change, mix and match elements from this tutorial to suit your needs. Initially I suggest changing the location of an element in the CSS style declaration, then viewing the result. Then work from there to move it around the page, change the width, height, colors, fonts, images, etc. so you can best reflect what you want to print. For a pre-printed format, change the border in the table CSS to 0, so that only the text is printed.
  2. If using a monochrome printer (thermal, or otherwise), use high contrasting colors: Black on white, etc. However I have not tried this on a high contrast printer, and do trial and error.

Suggestions and Comments

If you disagree with any portion of this, criticism will be welcome below, but only as an effective functioning suggestion. Critcism for criticism's sake is not welcome, as our goal is to improve this process for the user's benefits. Let's save any frustration and focus on better practices to simplify this.

References

Designing print format for pre-printed papers - ERPNext discussion forums

Clone this wiki locally