[Tutorial] Sign & Request Signatures online through HTML Print View

Hello!

We have recently started working on going mostly digital and zero paper for daily transactions. This decision has forced me to redesign our print formats to be;

  1. Standardized across all views (Responsive HTML, Print, PDF).
  2. Publicly accessible HTML view for most documents.
  3. Able to be digitally signed (per employee) without any external software.
  4. Able to receive customer/supplier signatures online, without the need of creating accounts for them.
  5. Password Protect sensitive information within the HTML/Print view

I decided to share this because it might be useful for some people with a similar scenario. All of the following has been tested on V15 across different devices.

Notes

  1. All of the following has been tested on V15 across different devices.
  2. Custom HTML Blocks within the standard Print Format Builder are used.
  3. Custom CSS Classes are written in the print format’s Custom CSS section.
    image

1. Standardized & Responsive Views and Prints
While designing print formats I work with flex (inside custom html blocks, inside print format sections or “containers”).
Flex is not fully supported by wkhtmltopdf so you have to prefix most properties, here is a useful stylesheet to use flex in your print designs.

– Shared in the next post due to char limits –

2. Publicly accessible HTML view

  • To allow publicly accessible web views you need to enable “Allow Older Web View Links (Insecure)” from your System Settings.
    image
  • To get the public view link of your document you can use the following jinja;
{% set doc_link = frappe.get_url() + '/' + doc.doctype + '/' + doc.name + '?key=' + frappe.get_doc(doc.doctype, doc.name).get_signature() %}
  • You can send this link to the party that needs to sign the document. Alternatively you can add a clickable QR code directly to your document so they scan (if printed) or click (if PDF) to reach the document.
<a href="{{doc_link}}"><img src="https://api.qrserver.com/v1/create-qr-code/?data={{doc_link | urlencode }}&amp;size=80x80&amp;color=00545c&amp;bgcolor=ffffff" alt="{{doc.name}}" title="{{doc.name}}" style="width: 80px; height: 80px;" /></a>

Example:
image

Note: your default print format shows in the HTML view.

3 & 4. Digitally Sign Documents (Employees) & Guests
Some documents are required to be signed by a specific employee, any employee, and/or stamped by the company. Some documents should also be signed by an external party such as a customer or a supplier.
I’ve already written the scripts and code to achieve this, but I’m too lazy to break it apart, so I’ll explain what it does, share the full codes and modifications and then you can modify accordingly. To limit who can sign a document, look for allowed_for in Server Script API as well as the API request from client side.

  1. On the HTML Web View of a document, you will see a form to enter a signature passcode (for employee or company)

  2. The employee will enter their passcode to get their signature
    image

  3. Once the API verifies if it’s the correct passcode, it will get the employee name based on the password, convert the signature to base64 image, return data back to client side.
    image

  4. To protect signatures from being stolen as raw pngs, I decided to watermark every signature with the document name. This watermarking process is done on the client side after the signature has been received from the API, using canvas.

  5. Canvas do not render if you attempt to extract the document as pdf, so we have to send the watermarked signature base64 url and store it somewhere in our document. Then fetch it using jinja.
    image
    As you can see the employee’s signature shows up watermarked, with the employee name and date of signature below.

  6. Now you might need to send this document to your customer to sign, we’ll use canvas to take the user’s signature. This also works on mobile devices and tablets.

  7. Customer/Supplier signs the document (This is mobile friendly too!)
    image

  8. Same flow goes for step 4 and 5, then it’s stored in the document and displayed.


    How it’s stored in the document

Example of how the signature will be rendered
image

Note: Since this is a guest-mode public API, we can not use POST method. Since base64 url is long, we chunk it up in a different child table then render the full url and then clear the table.

  • Create a Server Script as the following:
    image
    Add the following script:
docName = frappe.form_dict.docName
docType = frappe.form_dict.docType
allowed_for = frappe.form_dict.allowed_for
message = frappe.form_dict.message 
company = frappe.get_doc('Company', 'COMPANY NAME')
company_passcode = company.custom_company_passcode
signee = frappe.form_dict.signee
chunk = frappe.form_dict.get('chunk')
index = int(frappe.form_dict.get('index', -1))
total_chunks = int(frappe.form_dict.get('total', 0))
start = frappe.form_dict.get('start') == 'true'
end = frappe.form_dict.get('end') == 'true'

if start:
    doc = frappe.get_doc(docType, docName)
    # Clear existing chunks if any
    doc.set('custom_chunks', [])  # Assuming 'custom_chunks' is the field name for storing chunks
    doc.save()
    frappe.db.commit()

# Fetch the main document where you want to store the chunks
if chunk:
    doc = frappe.get_doc(docType, docName)

    # Add the chunk to the 'custom_chunks' child table
    new_chunk = {
        'chunk_index': index,
        'chunk_data': chunk
    }
    doc.append('custom_chunks', new_chunk)  # Ensure 'custom_chunks' is the correct field name
    doc.save()
    frappe.db.commit()
    
    # Check if all chunks have been received
    if end or (doc.custom_chunks and len(doc.custom_chunks) == total_chunks):
        # Sort and concatenate the chunks
        sorted_chunks = sorted(doc.custom_chunks, key=lambda x: x.chunk_index)
        full_base64_string = ''.join(chunk.chunk_data for chunk in sorted_chunks)
        
        # Append to the 'custom_signature_table' child table
        doc.append("custom_signature_table", {
            'signee': signee,
            'signee_full_name': signee,
            'signature_date': frappe.utils.formatdate(frappe.utils.now(), "dd-MMM-yyyy"),
            'signature': full_base64_string
        })
        
        # Clear the 'custom_chunks' after processing, if desired
        doc.set('custom_chunks', [])
        
        doc.save()
        frappe.db.commit()

        frappe.response['message'] = "Signature processed successfully"
    else:
        frappe.response['message'] = f"Chunk {index} received, waiting for more"


if allowed_for == "employee":
    employees = frappe.get_all('Employee', fields=['custom_employee_passcode', 'name', 'first_name', 'last_name', 'custom_employee_signature'])
    for employee in employees:
        if message == employee['custom_employee_passcode']:
            frappe.response['message'] = "true"
            frappe.response['date'] = frappe.utils.formatdate(frappe.utils.now(), "dd-MMM-yyyy")
            frappe.response['signee'] = employee['name']
            custom_employee_signature = employee.get('custom_employee_signature')
            if custom_employee_signature is not None:
                frappe.response['signature'] = custom_employee_signature
            else:
                frappe.response['signature'] = ""
            first_name = employee['first_name']
            last_name = employee['last_name']
            if first_name is not None:
                if last_name is not None and last_name is not "":
                    full_name = first_name + ' ' + last_name
                else:
                    full_name = first_name
            else:
                full_name = "Failed to fetch name."
            frappe.response['full_name'] = full_name
            doc = frappe.get_doc(docType, docName)
            break  # Stop checking further employees once a match is found
        else:
            if message == company_passcode:
                frappe.response['message'] = "true"
                frappe.response['date'] = frappe.utils.formatdate(frappe.utils.now(), "dd-MMM-yyyy")
                frappe.response['signee'] = company.name  # Set the company name as the signee
                frappe.response['full_name'] = company.name
                frappe.response['signature'] = company.get('custom_company_stamp')
            else:
                frappe.response['message'] = "false"  # No matching employee or company passcode found
else:
    frappe.response['message'] = "false"  # No matching employee or company passcode found
  • In the document that wants to receive the signatures add this as an HTML Block. This takes an employee signature as well as a client signature. Modify as needed. Note: Most of the classes are just for styling purposes, the required ones are shared above.
<div class="m -row-r -w-100 -j-space-between -cards -middle" style="page-break-inside: avoid; width: 100%">
    <div>
        {% if doc.docstatus != 2 %}
            {% if doc.custom_signature_table %}
            {% set signee = doc.custom_signature_table[0] %}
            {% set signature = 'data:image/png;base64,' + signee.signature %}
            {% endif %}
            <div class="signature-container" style="margin-top: 40px; height: 4cm; width: 8cm; page-break-inside: avoid !important;">
             <div class="signature-background"></div>
             <div class="signature">
                {% for _ in range(19) %}
                    <div class="signature-text">{{ doc.name }}</div>
                {% endfor %}
                <div class="m -col -middle signature-content {% if not doc.custom_signature_table %}-center{% else %}-j-space-between{% endif %}" 
                style="
                height: 100%; 
                width: 100%; 
                {% if doc.custom_signature_table %}
                background-image: url('{{signature}}'); 
                background-position: center;
                background-repeat: no-repeat;
                background-size: contain
                {% endif %}" 
                id="signatureHolder">
                {% if doc.custom_signature_table %}
                    <div style="margin-top: 10px; font-size: 9px; font-weight: 600;" class="-secondary">{{doc.doctype}}: {{doc.name}}</div>
                    <div style="margin-bottom: 10px; font-size: 9px; font-weight: 600;" class="-secondary">Received by {{signee.signee}} on {{signee.signature_date}}</div>
                {% else %}
                    <form class="-rcf -print-hide -col" id="rcf1" autocomplete="off">
                        <label class="-rcf-label -h6" for="passcode"><strong class="-secondary">Sign This Document</strong></label>
                        <div class="-rcf-input-group">
                        <input class="-rcf-input" placeholder="Staff Signature Passcode" type="text"  id="passcode" name="passcode">
                        <span class="-rcf-icon">đź”’</span>
                        <button class="-rcf-button" type="button" id="submitBtn">Submit</button>
                        </div>
                        <div class="-rcf-footer">Hello</div>
                        <div class="-rcf-details" id="rcfDetails" style="margin: 0"></div>
                    </form>
                {% endif %}
                </div>
             </div>
            </div>
        
        {% endif %}
    </div>
    <div>
        {% if doc.docstatus != 2 and doc.custom_signature_table[1]  %}
            {% set client = doc.custom_signature_table[1] %}
            {% set client_signature = 'data:image/png;base64,' + client.signature %}
            <div class="signature-container" style="margin-top: 40px; height: 4cm; width: 8cm; page-break-inside: avoid !important;">
             <div class="signature-background"></div>
             <div class="signature">
                {% for _ in range(19) %}
                    <div class="signature-text">{{ doc.name }}</div>
                {% endfor %}
                <div class="m -col -middle signature-content -j-space-between" 
                style="
                height: 100%; 
                width: 100%; 
                background-image: url('{{client_signature}}'); 
                background-position: center;
                background-repeat: no-repeat;
                background-size: contain" 
                id="signatureHolder2">
                    <div style="margin-top: 10px; font-size: 9px; font-weight: 600;" class="-secondary">{{doc.doctype}}: {{doc.name}}</div>
                    <div style="margin-bottom: 10px; font-size: 9px; font-weight: 600;" class="-secondary">Delivered by {{client.signee}} on {{client.signature_date}}</div>
                </div>
             </div>
            </div>
            {% elif doc.docstatus != 2 and doc.custom_signature_table[0] and not doc.custom_signature_table[1] %}
            <div class="signature-container" style="margin-top: 40px; height: 4cm; width: 8cm; page-break-inside: avoid !important;">
             <div class="signature-background"></div>
             <div class="signature">
                {% for _ in range(19) %}
                    <div class="signature-text">{{ doc.name }}</div>
                {% endfor %}
                <div class="m -col -middle signature-content -j-space-between" style="height: 100%; width: 100%;">
            
                    <div style="margin-top: 10px; font-size: 9px; font-weight: 600;" class="-secondary">{{doc.doctype}}: {{doc.name}}</div>
                    <div style="margin-bottom: 10px; font-size: 9px; font-weight: 600;" class="-secondary">Please Sign Below</div>
            
                </div>
             </div>
            </div>
            {% else %}
            <div></div>
        {% endif %}
    </div>
</div>
  • Create another HTML Block to accept guest signature.
<div id="loadingPopup" class="popup">
    <div class="popup-content">
        <div class="loading-spinner"></div>
    </div>
</div>

{% if doc.custom_signature_table[0] and not doc.custom_signature_table[1] %}
	<div class="m -col" id="signatureRequest" style="margin-top: 40px; border: 3px solid #00545c; border-radius: 15px; padding: 10px;">
		<div class="m -col -w-100" style="width: 100%;">
				<div class="-h3 -secondary">Mirage E-Signature</div>
				<div class="-p -black"><strong>Thank You for Delivering Our Order!<br></strong>Please sign below to confirm delivery. </div>
    			<div style="width: 100%">
    		 		<canvas id="sig-canvas" width="320" height="160">
    		 			Please open this in Google Chrome.
    		 		</canvas>
		        </div>
		        <div>
                    <label for="signee-name">Signee Name:</label>
                    <input type="text" id="signee-name" style="width: 320px;" class="form-control" placeholder="Enter your name" required>
                </div>
		        <div style="margin-top: 10px;">
    				<button class="btn -secondary-bg -white" id="sig-submitBtn">Submit Signature</button>
    				<button class="btn btn-default" id="sig-clearBtn">Clear Signature</button>
		        </div>
        </div>

	</div>
<style>
    #sig-canvas {
        margin-top: 20px;
  border: 2px dotted #00545c;
  border-radius: 15px;
  cursor: crosshair;
}
.popup {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    z-index: 999;
  }

.popup-content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    background: #fff;
    padding: 20px; /* Increase padding */
    border-radius: 5px;
}


  .loading-spinner {
    border: 4px solid rgba(0, 0, 0, 0.3);
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
    margin: 0 auto;
  }

  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
</style>
<script>
var loadingText = document.createElement('p');
document.querySelector('.popup-content').appendChild(loadingText);

var texts = ["Loading...", "Do not refresh the page!", "Encrypting Your Signature...", "Applying watermark to signature...", "Uploading to server..."];
var i = 0;

setInterval(function () {
    loadingText.textContent = texts[i % texts.length];
    i++;
}, 2000); // Change text every 2 seconds

    (function() {
  window.requestAnimFrame = (function(callback) {
    return window.requestAnimationFrame ||
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame ||
      window.oRequestAnimationFrame ||
      window.msRequestAnimaitonFrame ||
      function(callback) {
        window.setTimeout(callback, 1000 / 60);
      };
  })();
  var canvasContainer = document.getElementById("signatureRequest");

canvasContainer.addEventListener("touchstart", function (e) {
    if (e.target == canvas) {
        e.preventDefault();
        canvasContainer.style.overflow = "hidden"; // Disable scrolling
    }
}, false);

canvasContainer.addEventListener("touchend", function (e) {
    if (e.target == canvas) {
        e.preventDefault();
        canvasContainer.style.overflow = "auto"; // Enable scrolling
    }
}, false);

canvasContainer.addEventListener("touchmove", function (e) {
    if (e.target == canvas) {
        e.preventDefault();
    }
}, false);

  var canvas = document.getElementById("sig-canvas");
  var ctx = canvas.getContext("2d");
  ctx.strokeStyle = "#7f7f7f";
  ctx.lineWidth = 4;

  var drawing = false;
  var mousePos = {
    x: 0,
    y: 0
  };
  var lastPos = mousePos;

  canvas.addEventListener("mousedown", function(e) {
    drawing = true;
    lastPos = getMousePos(canvas, e);
  }, false);

  canvas.addEventListener("mouseup", function(e) {
    drawing = false;
  }, false);

  canvas.addEventListener("mousemove", function(e) {
    mousePos = getMousePos(canvas, e);
  }, false);

  // Add touch event support for mobile
  canvas.addEventListener("touchstart", function(e) {

  }, false);

  canvas.addEventListener("touchmove", function(e) {
    var touch = e.touches[0];
    var me = new MouseEvent("mousemove", {
      clientX: touch.clientX,
      clientY: touch.clientY
    });
    canvas.dispatchEvent(me);
  }, false);

  canvas.addEventListener("touchstart", function(e) {
    mousePos = getTouchPos(canvas, e);
    var touch = e.touches[0];
    var me = new MouseEvent("mousedown", {
      clientX: touch.clientX,
      clientY: touch.clientY
    });
    canvas.dispatchEvent(me);
  }, false);

  canvas.addEventListener("touchend", function(e) {
    var me = new MouseEvent("mouseup", {});
    canvas.dispatchEvent(me);
  }, false);

  function getMousePos(canvasDom, mouseEvent) {
    var rect = canvasDom.getBoundingClientRect();
    return {
      x: mouseEvent.clientX - rect.left,
      y: mouseEvent.clientY - rect.top
    }
  }

  function getTouchPos(canvasDom, touchEvent) {
    var rect = canvasDom.getBoundingClientRect();
    return {
      x: touchEvent.touches[0].clientX - rect.left,
      y: touchEvent.touches[0].clientY - rect.top
    }
  }

  function renderCanvas() {
    if (drawing) {
      ctx.moveTo(lastPos.x, lastPos.y);
      ctx.lineTo(mousePos.x, mousePos.y);
      ctx.stroke();
      lastPos = mousePos;
    }
  }

  // Prevent scrolling when touching the canvas
  document.body.addEventListener("touchstart", function(e) {
    if (e.target == canvas) {
      e.preventDefault();
    }
  }, false);
  document.body.addEventListener("touchend", function(e) {
    if (e.target == canvas) {
      e.preventDefault();
    }
  }, false);
  document.body.addEventListener("touchmove", function(e) {
    if (e.target == canvas) {
      e.preventDefault();
    }
  }, false);

  (function drawLoop() {
    requestAnimFrame(drawLoop);
    renderCanvas();
  })();

  function clearCanvas() {
    canvas.width = canvas.width;
  }

  // Set up the UI
  var clearBtn = document.getElementById("sig-clearBtn");
  var submitBtn = document.getElementById("sig-submitBtn");
  var docName = "{{doc.name}}";
  var docType = "{{doc.doctype}}";
  clearBtn.addEventListener("click", function(e) {
    clearCanvas();
    ctx.strokeStyle = "#7f7f7f";
    ctx.lineWidth = 4;
  }, false);
  submitBtn.addEventListener("click", function(e) {
    var dataUrl = canvas.toDataURL(); // Capture the signature
    var signeeName = document.getElementById("signee-name").value.trim(); // Capture the signee's name
    var signatureRequest = document.getElementById("signatureRequest")
    // Validate signee name
    if (signeeName === "") {
        alert("Please enter your name.");
        return;
    }
    signatureRequest.innerHTML = ""
    var loadingPopup = document.getElementById("loadingPopup");
    loadingPopup.style.display = "block";
    // Apply watermark with signee's name
    addWatermark(dataUrl, function(watermarkedImage) {
        // Convert watermarked image to chunks
        var base64Data = watermarkedImage.src.split(',')[1]; // Extract base64 data
        var chunks = chunkBase64Data(base64Data, 1000); // Assuming a chunk size of 1000, adjust as needed
        
        // Send chunks to the server
        sendChunks(chunks, 0, signeeName, docName, docType);
    }, 20, 100); // Adjust verticalSpacing, horizontalSpacing as needed
}, false);

})();
</script>
{% endif %}
  • One last HTML Block, this time for the client-side logic.
{% if doc.docstatus != 2 %}
<script>
var formElement = document.getElementById("-rcf-1");
if (formElement) {
    formElement.addEventListener("submit", function(event){
        event.preventDefault();
    });
}

var submitButton = document.getElementById("submitBtn");
if (submitButton) {
    submitButton.addEventListener("click", function(){
        callAPI();
    });
}

var passcodeInput = document.getElementById("passcode");
if (passcodeInput) {
    passcodeInput.addEventListener("keypress", function(event){
        if (event.keyCode === 13 || event.which === 13) { 
            event.preventDefault(); 
            callAPI();
        }
    });

    passcodeInput.addEventListener("focus", function(){
        var detailsElement = document.getElementById("rcfDetails");
        if (detailsElement) {
            detailsElement.innerHTML = ''; // Clear error message
        }
        this.value = ''; // Clear input field content
    });
}
var loader = document.getElementById('loader'); 
function preventNavigation(event) {
    // This message may not be shown in modern browsers,
    // but a default browser-specific message will be shown instead.
    var message = 'Upload in progress. Are you sure you want to leave?';
    event.returnValue = message; // Standard for most browsers
    return message; // For older browsers
}

function enableNavigationWarning() {
    window.addEventListener('beforeunload', preventNavigation);
}

function disableNavigationWarning() {
    window.removeEventListener('beforeunload', preventNavigation);
}

function showLoader() {
    if (loader){
    loader.style.display = 'block';
    }
}

function hideLoader() {
    if (loader){
    loader.style.display = 'none';
    }
}
// Define the addWatermark function
function addWatermark(imgSrc, callback, verticalSpacing, horizontalSpacing) {
    // Create a new Image object to load the image
    var image = new Image();
    
    // Set an event listener for when the image loads
    image.onload = function() {
        // Create a canvas and get its context
        var canvas = document.createElement('canvas');
        canvas.width = image.width;
        canvas.height = image.height;
        var ctx = canvas.getContext('2d');

        // Draw the original image onto the canvas
        ctx.drawImage(image, 0, 0);

        // Set the style for the watermark text
        ctx.fillStyle = '#7a7a7a'; 
        ctx.font = '14px Arial';
        ctx.textAlign = 'center'; // Center text horizontally
        ctx.textBaseline = 'middle'; // Center text vertically

        // Add the watermark text at multiple vertical and horizontal positions
        var watermarkText = '{{doc.name}}';
        var textY = 35; // Start from the top
        while (textY < canvas.height - 25) {
            var textX = 0; // Start from the left
            while (textX < canvas.width) {
                ctx.fillText(watermarkText, textX, textY);
                textX += horizontalSpacing;
            }
            textY += verticalSpacing;
        }

        // Convert the canvas back to an image
        var watermarkedImage = new Image();
        watermarkedImage.src = canvas.toDataURL();

        // Callback with the watermarked image
        callback(watermarkedImage);
    };

    // Set the source of the Image object
    image.src = imgSrc;
}

function chunkBase64Data(data, size) {
    var chunks = [];
    for (var i = 0; i < data.length; i += size) {
        chunks.push(data.substring(i, i + size));
    }
    return chunks;
}
function sendChunks(chunks, index, fullName, docName, docType) {
    if (index >= chunks.length) {
        console.log('All chunks sent successfully');
        disableNavigationWarning()
        if (loader){
        loader.innerHTML = 'Success! Please reload the page.';
        }
        location.reload();
        return; // Stop condition
    }
    
    var chunk = chunks[index];
    var url = '/api/method/YOUR METHOD NAME/signatureapi?chunk=' + encodeURIComponent(chunk) + '&index=' + index + '&total=' + chunks.length + '&signee=' + encodeURIComponent(fullName) + '&docName=' + encodeURIComponent(docName) + '&docType=' + encodeURIComponent(docType);
    fetch(url)
        .then(response => response.json())
        .then(data => {
            enableNavigationWarning()
            if (loader){
            loader.style.display = 'block';
            }
            console.log('Chunk ' + index + ' sent successfully');
            sendChunks(chunks, index + 1, fullName, docName, docType); // Send next chunk
        })
        .catch(error => console.error('Error sending chunk ' + index, error));
}
function callAPI() {
    var text = document.getElementById('passcode').value;
    var allowed_for = "employee";
    var docName = "{{doc.name}}";
    var docType = "{{doc.doctype}}";
    
    fetch('/api/method/YOUR METHOD NAME/signatureapi?message=' + encodeURIComponent(text) + '&allowed_for=' + encodeURIComponent(allowed_for) + '&docName=' + encodeURIComponent(docName) + '&docType=' + encodeURIComponent(docType))
    .then(response => response.json())
    .then(data => {
        if (data.message === "true") {
            
            var signatureHolder = document.getElementById("signatureHolder");
            if (data.signee) {
                // Get the watermark image as a base64 data URL
                addWatermark(data.signature, function(watermarkImage) {
                    // Use a vertical spacing of 20 pixels between watermark text
                    addWatermark(data.signature, function(watermarkImage) {
                        signatureHolder.innerHTML = '';
                        signatureHolder.innerHTML = '<div class="loadingio-spinner-rolling-ku9ovaw400i"><div class="ldio-jy81f04els"><div></div></div></div><style type="text/css">@keyframes ldio-jy81f04els{0%{transform:translate(-50%,-50%) rotate(0)}100%{transform:translate(-50%,-50%) rotate(360deg)}}.ldio-jy81f04els div{position:absolute;width:21.669999999999998px;height:21.669999999999998px;border:5.91px solid #00545c;border-top-color:transparent;border-radius:50%}.ldio-jy81f04els div{animation:ldio-jy81f04els .5813953488372093s linear infinite;top:98.5px;left:98.5px}.loadingio-spinner-rolling-ku9ovaw400i{width:197px;height:197px;display:inline-block;overflow:hidden;background:0 0}.ldio-jy81f04els{width:100%;height:100%;position:relative;transform:translateZ(0) scale(1);backface-visibility:hidden;transform-origin:0 0}.ldio-jy81f04els div{box-sizing:content-box}</style>'
                        
                        // Encode the watermarked image source for the API request
                    var base64Data = watermarkImage.src.split(',')[1]; // Remove the data URL prefix
                    var chunks = chunkBase64Data(base64Data, 1000); // Example: chunk size of 10000
                    sendChunks(chunks, 0, data.full_name, docName, docType);
                        
                    }, 20, 100);
                });


                signatureHolder.addEventListener('contextmenu', function (e) {
                    e.preventDefault();
                });

            } else {
                sigsignee.innerHTML = 'Employee not found'; // Handle case when no employee name is returned
            }
        } else {
            var rcfDetails = document.getElementById("rcfDetails");
            rcfDetails.innerHTML = '<small class="-red" style="font-weight: 600">Incorrect Passcode.</small>'
        }
    })
    .catch((error) => {
      console.error('Error:', error);
    });
}

</script>
{% endif %}
  • Modify Employee Doctype to have:
  1. Field to store their passkey or password (used to acquire the signature) (Let’s call it custom_employee_passcode and make it of type “Data”) - make sure it has higher field permissions so not anyone can view/edit this.
  2. Field to get their signature (in our case it’s a signature field, but you can also use an attach image field) Let’s call it “custom_employee_signature”
    image
  • Modify Company Doctype to have
  1. Field to store company-wide passkey or password (used to acquire the stamp) (Let’s call it custom_company_passcode and make it of type “Data”) - make sure it has higher field permissions so not anyone can view/edit this.
  2. Field to store the company stamp (in our case it’s an attach field) Let’s call it “custom_company_stamp”

Finally,
For every document you want to sign or receive signatures add those:

Chunks table will temporarily hold chunked base64 data until all combined then clears itself. It also clears itself before every signature upload process.

Test and modify the scripts as needed. MAKE SURE TO REPLACE METHOD NAME IN CLIENT-SIDE FUNCTIONS INSIDE THE PRINT FORMAT

5. Password Protect sensitive information within the HTML/Print view
Since we enabled public view, some content might need to be hidden and password protected from within the print format. (You still want it in the print format but a passkey is required to access it). I will share this later or on request. Got bored from this =)

9 Likes

CSS to use Flexbox that works with wkhtmltopdf.

.m{
  -webkit-display: flex;
  -moz-display: flex;
  -ms-display: flex;
  -o-display: flex;
  display: -webkit-box;
  display: -moz-box;
  display: -ms-flexbox;
  display:-webkit-flex;
  display: flex;
}
.-row{
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
   -moz-box-orient: horizontal;
   -moz-box-direction: normal;
    -ms-flex-direction: row;
        flex-direction: row;
}
.-row-r{
-webkit-box-orient: horizontal;
-webkit-box-direction: reverse;
-webkit-flex-direction: row-reverse;
   -moz-box-orient: horizontal;
   -moz-box-direction: reverse;
    -ms-flex-direction: row-reverse;
        flex-direction: row-reverse;
}
.-col{
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
   -moz-box-orient: vertical;
   -moz-box-direction: normal;
    -ms-flex-direction: column;
        flex-direction: column;
}
.-col-r{
-webkit-box-orient: vertical;
-webkit-box-direction: reverse;
-webkit-flex-direction: column-reverse;
   -moz-box-orient: vertical;
   -moz-box-direction: reverse;
    -ms-flex-direction: column-reverse;
        flex-direction: column-reverse;
}
.-wrap{
-webkit-flex-wrap: wrap;
    -ms-flex-wrap: wrap;
        flex-wrap: wrap;
}
.-no-wrap{
-webkit-flex-wrap: nowrap;
    -ms-flex-wrap: nowrap;
        flex-wrap: nowrap;
}
.-left{
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
   -moz-box-pack: start;
    -ms-flex-pack: start;
        justify-content: flex-start;
}
.-right{
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
   -moz-box-pack: end;
    -ms-flex-pack: end;
        justify-content: flex-end;
}
.-center{
-webkit-box-pack: center;
-webkit-justify-content: center;
   -moz-box-pack: center;
    -ms-flex-pack: center;
        justify-content: center;
}
.-j-space-between{
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
   -moz-box-pack: justify;
    -ms-flex-pack: justify;
        justify-content: space-between;
}
.-j-space-around{
-webkit-justify-content: space-around;
    -ms-flex-pack: distribute;
        justify-content: space-around;
}
.-j-space-evenly{
-webkit-box-pack: space-evenly;
-webkit-justify-content: space-evenly;
   -moz-box-pack: space-evenly;
    -ms-flex-pack: space-evenly;
        justify-content: space-evenly;
}
.-shrink{
-webkit-flex-shrink: 1;
    -ms-flex-negative: 1;
        flex-shrink: 1;
}
.-no-shrink{
-webkit-flex-shrink: 0;
    -ms-flex-negative: 0;
        flex-shrink: 0;
}
.-w-1{
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
   -moz-box-flex: 1;
    -ms-flex-positive: 1;
        flex-grow: 1;
}
.-w-2{
-webkit-box-flex: 2;
-webkit-flex-grow: 2;
   -moz-box-flex: 2;
    -ms-flex-positive: 2;
        flex-grow: 2;
}
.-w-3{
-webkit-box-flex: 3;
-webkit-flex-grow: 3;
   -moz-box-flex: 3;
    -ms-flex-positive: 3;
        flex-grow: 3;
}
.-w-4{
-webkit-box-flex: 4;
-webkit-flex-grow: 4;
   -moz-box-flex: 4;
    -ms-flex-positive: 4;
        flex-grow: 4;
}
.-top{
-webkit-box-align: start;
-webkit-align-items: flex-start;
   -moz-box-align: start;
    -ms-flex-align: start;
        align-items: flex-start;
}
.-bottom{
-webkit-box-align: end;
-webkit-align-items: flex-end;
   -moz-box-align: end;
    -ms-flex-align: end;
        align-items: flex-end;
}
.-middle{
-webkit-box-align: center;
-webkit-align-items: center;
   -moz-box-align: center;
    -ms-flex-align: center;
        align-items: center;
}
.-ml-start{
   -webkit-flex-wrap: wrap;
       -ms-flex-wrap: wrap;
           flex-wrap: wrap;
   -webkit-align-content: flex-start;
       -ms-flex-line-pack: start;
           align-content: flex-start;
}
.-ml-end{
   -webkit-flex-wrap: wrap;
       -ms-flex-wrap: wrap;
           flex-wrap: wrap;
   -webkit-align-content: flex-end;
       -ms-flex-line-pack: end;
           align-content: flex-end;
}
.-ml-center{
   -webkit-flex-wrap: wrap;
       -ms-flex-wrap: wrap;
           flex-wrap: wrap;
   -webkit-align-content: center;
       -ms-flex-line-pack: center;
           align-content: center;
}
.-ml-space-between{
   -webkit-flex-wrap: wrap;
       -ms-flex-wrap: wrap;
           flex-wrap: wrap;
   -webkit-align-content: space-between;
       -ms-flex-line-pack: justify;
           align-content: space-between;
}
.-ml-space-around{
   -webkit-flex-wrap: wrap;
       -ms-flex-wrap: wrap;
           flex-wrap: wrap;
   -webkit-align-content: space-around;
       -ms-flex-line-pack: distribute;
           align-content: space-around;
}
.-ml-space-evenly{
   -webkit-flex-wrap: wrap;
       -ms-flex-wrap: wrap;
           flex-wrap: wrap;
   -webkit-align-content: space-evenly;
       -ms-flex-line-pack: space-evenly;
           align-content: space-evenly;
}
.-pb-avoid{
  page-break-inside: avoid !important;
}
.-pb-before{
  page-break-before: always !important;
}
.-pb-after{
  page-break-after: always !important;
}

  • Copy paste in ChatGPT and let it right for you a reference sheet.
  • For divs containing only text you might need to set their “width: 100%”
  • These classes work perfectly on HTML View, Prints, as well as PDF renders.

CSS for Signature Forms.

.-rcf{
            background: #fff;
            padding: 20px;
            border-radius: 15px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            /*border: 1px solid #cc3333;*/
            position: relative;
}
.-rcf-label{
    margin-bottom: 10px;
    color: #333;
}
.-rcf-input{
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    box-sizing: border-box;
    flex-grow: 1; /* Make input take available space */
    border-radius: 4px 0 0 4px; /* Rounded corners only on left side */
    margin-bottom: 0; /* Remove bottom margin */
    padding-left: 30px;
}
.-rcf-button{
    background-color: #00838f;
    color: white;
    padding: 10px 10px;
    border: none;
    height: calc(1.2em + 25px); /* Adjust to match the input field height */
    border-radius: 0 4px 4px 0; /* Rounded corners only on the right side */
    margin-left: -4px; /* Align seamlessly with the input field */
    cursor: pointer;
    font-size: 16px;
}
.-rcf-button:hover{
    background-color: #00545c;
}
.-rcf-input-group {
    position: relative;
    display: flex;
}
.-rcf-icon {
    position: absolute;
    height: 16px; /* Adjust as needed */
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none; /* Makes the icon non-interactive */
}
.-rcf-input:focus {
    outline: none; /* Removes the default focus outline */
    border: 1px solid #00838f; /* Sets the border color to #00838f on focus */
}
.-rcf-footer {
    font-size: 8px; /* Small font size */
    padding: 0; /* Add some space above the footer */
    color: #666; /* Optional: change the color if needed */
    position: absolute;
    bottom: 2px;
    box-sizing: border-box;
    width: 80%;
    margin: 0 auto;
    text-align: center;
}
@media screen and (max-width: 600px) { /* Adjust 600px to your desired breakpoint */
    .-mobile-hidden{
        display: none;
    }
    .-rcf-input-group {
        flex-direction: column;
    }

    .-rcf-input {
        width: 100%; /* Full width on mobile */
        margin-bottom: 0; /* No space above the button */
        border-radius: 4px 4px 0 0; /* Rounded corners only at the top */
        padding-left: 10px; /* Adjust padding to align placeholder text */
        box-sizing: border-box; /* Ensure padding and border are included in width */
        font-size: 8px;
        height: 33px;
        
    }

    .-rcf-icon {
        display: none; /* Hide the lock icon on mobile */
    }

    .-rcf-button {
        width: 100%; /* Full width on mobile */
        border-radius: 0 0 4px 4px; /* Rounded corners only at the bottom */
        box-sizing: border-box; /* Ensure padding and border are included in width */
        margin-left: 0;
        margin-top: -5px;
        margin-bottom: 5px;
        height: 28px;
        padding: 0;
    }
}
3 Likes

thanks for sharing, is it possible to share a sample github repository for nore easy reference and adaptation as needed.

thanks this is helpful!