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;
- Standardized across all views (Responsive HTML, Print, PDF).
- Publicly accessible HTML view for most documents.
- Able to be digitally signed (per employee) without any external software.
- Able to receive customer/supplier signatures online, without the need of creating accounts for them.
- 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
- All of the following has been tested on V15 across different devices.
- Custom HTML Blocks within the standard Print Format Builder are used.
- Custom CSS Classes are written in the print format’s Custom CSS section.
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.
- 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 }}&size=80x80&color=00545c&bgcolor=ffffff" alt="{{doc.name}}" title="{{doc.name}}" style="width: 80px; height: 80px;" /></a>
Example:
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.
-
On the HTML Web View of a document, you will see a form to enter a signature passcode (for employee or company)
-
The employee will enter their passcode to get their signature
-
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.
-
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.
-
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.
As you can see the employee’s signature shows up watermarked, with the employee name and date of signature below. -
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.
-
Customer/Supplier signs the document (This is mobile friendly too!)
-
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
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:
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:
- 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.
- 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”
- Modify Company Doctype to have
- 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.
- Field to store the company stamp (in our case it’s an attach field) Let’s call it “custom_company_stamp”
-
Create This Doctype (MAKE SURE IT’S A CHILD TABLE, ENABLE IN LIST VIEW FOR FIELDS)
-
Create Another Doctype (SAME NOTICE AS ABOVE)
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 =)