Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sign_stamp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
33 changes: 33 additions & 0 deletions sign_stamp/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
'author': 'Odoo S.A.',
'name': 'Sign Stamp',
'description': """
Adds a Company Stamp feature to the Sign application.
This module introduces a new Stamp sign item that allows companies to
apply an official stamp containing company details such as name, address,
VAT number, and logo. The stamp is rendered dynamically and can be used
as a valid electronic signature when signing documents.
""",
'depends': ['sign', 'web'],
'data': [
'data/sign_data.xml',
'views/sign_request_templates.xml'
],
'assets': {
'web.assets_backend': [
'sign_stamp/static/src/components/**/*',
'sign_stamp/static/src/dialogs/**/*',
],
'web.assets_frontend': [
'sign_stamp/static/src/components/**/*',
'sign_stamp/static/src/dialogs/**/*',
],
'sign.assets_public_sign': [
'sign_stamp/static/src/components/**/*',
'sign_stamp/static/src/dialogs/**/*',
],
},
'license': 'LGPL-3',
'application': True,
'installable': True
}
16 changes: 16 additions & 0 deletions sign_stamp/data/sign_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="sign.sign_item_type_signature" model="sign.item.type">
<field name="sequence">0</field>
</record>

<record id="sign_item_type_test_stamp" model="sign.item.type">
<field name="name"></field>
<field name="sequence">0</field>
<field name="item_type">stamp</field>
<field name="placeholder">Company&#10;Address&#10;City&#10;Country&#10;VAT</field>
<field name="default_width" type="float">0.3</field>
<field name="default_height" type="float">0.1</field>
<field name="icon">fa-circle</field>
</record>
</odoo>
1 change: 1 addition & 0 deletions sign_stamp/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import sign_item_type
17 changes: 17 additions & 0 deletions sign_stamp/models/sign_item_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from odoo import api, fields, models


class SignItemType(models.Model):
_inherit = "sign.item.type"
_order = "sequence"

sequence = fields.Integer(string="Sequence", default=1)
display_name = fields.Char(compute="_compute_display_name")

@api.depends_context('company')
def _compute_display_name(self):
self.display_name = self.env.company.name
for record in self:
if record.item_type == "stamp" and record.sequence == 0:
record.name = record.display_name
break
23 changes: 23 additions & 0 deletions sign_stamp/static/src/components/sign_request/document_signable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { patch } from "@web/core/utils/patch";
import { Document } from "@sign/components/sign_request/document_signable";

patch(Document.prototype, {
getDataFromHTML() {
super.getDataFromHTML();
const { el: parentEl } = this.props.parent;
this.companyInfo = {};
this.companyInfo.company = parentEl.querySelector("#o_sign_signer_company_input_info")?.value;
this.companyInfo.address = parentEl.querySelector("#o_sign_signer_address_input_info")?.value;
this.companyInfo.city = parentEl.querySelector("#o_sign_signer_city_input_info")?.value;
this.companyInfo.country = parentEl.querySelector("#o_sign_signer_country_input_info")?.value;
this.companyInfo.vat = parentEl.querySelector("#o_sign_signer_vat_input_info")?.value;
},

getIframeProps(sign_document_id) {
const props = super.getIframeProps(sign_document_id);
return {
...props,
companyInfo: this.companyInfo
};
},
});
17 changes: 17 additions & 0 deletions sign_stamp/static/src/components/sign_request/sign_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="sign.signItem" t-inherit-mode="extension">
<xpath expr="//t[@t-if='!readonly']" position="inside">
<button t-if="type == 'stamp'" type="button" t-att-title="role" t-attf-class="{{classes}} o_sign_sign_item o_sign_stamp text-center" t-att-style="style" t-att-data-signature="value">
<span class="o_sign_helper"/>
<img t-if="frame_value" t-att-src="frame_value" alt="Frame"/>
<img t-if="value" t-att-src="value" alt="Stamp"/>
<t t-if="!value">
<span class="o_placeholder ps-0">
<t t-esc="placeholder"/>
</span>
</t>
</button>
</xpath>
</t>
</templates>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { patch } from "@web/core/utils/patch";
import { SignablePDFIframe } from "@sign/components/sign_request/signable_PDF_iframe";
import { SignNameAndSignatureDialog } from "@sign/dialogs/dialogs";
import { StampSignDetailsDialog } from "../../dialogs/stamp_dialog";

patch(SignablePDFIframe.prototype, {
enableCustom(signItem) {
super.enableCustom(signItem);
const signItemType = this.signItemTypesById[signItem.data.type_id];
if (!signItemType || signItemType.item_type !== "stamp") {
return;
}
signItem.el.addEventListener("click", (e) => {
this.openSignatureDialog(e.currentTarget, signItemType);
});
},

openSignatureDialog(signatureItem, type) {
if (this.dialogOpen) {
return;
}
const signature = {
name: this.signerName || "",
company: this.props.companyInfo?.company || "",
address: this.props.companyInfo?.address || "",
city: this.props.companyInfo?.city || "",
country: this.props.companyInfo?.country || "",
vat: this.props.companyInfo?.vat || "",
};
const frame = {};
const { height, width } = signatureItem.getBoundingClientRect();
const signFrame = signatureItem.querySelector(".o_sign_frame");
this.dialogOpen = true;
this.closeFn = this.dialog.add(
type.item_type === "stamp" ? StampSignDetailsDialog : SignNameAndSignatureDialog,
{
frame,
signature,
signatureType: type.item_type,
displaySignatureRatio: width / height,
activeFrame: Boolean(signFrame) || !type.auto_value,
mode: "auto",
defaultFrame: type.frame_value || "",
hash: this.frameHash,
onConfirm: async () => {
if (!signature.isSignatureEmpty && signature.signatureChanged) {
const signatureName = signature.name;
this.props.updateSignerName(signatureName);
await frame.updateFrame();
const frameData = frame.getFrameImageSrc();
const signatureSrc = signature.getSignatureImage();
this.fillItemWithSignature(signatureItem, signatureSrc, {
frame: frameData,
hash: this.frameHash,
});
}
this.closeDialog();
this.handleInput();
},
},
{
onClose: () => {
this.dialogOpen = false;
},
}
);
},

getSignatureValueFromElement(item) {
return item.data.type === "stamp" ? item.el.dataset.signature : super.getSignatureValueFromElement(item)
},
});
45 changes: 45 additions & 0 deletions sign_stamp/static/src/dialogs/name_and_sign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { renderToString } from "@web/core/utils/render";
import { patch } from "@web/core/utils/patch";
import { NameAndSignature } from "@web/core/signature/name_and_signature";

patch(NameAndSignature.prototype, {
async drawCurrentName() {
if (this.props.signatureType === "stamp") {
const font = this.fonts[this.currentFont];
const stamp = this.getStampDetails();
const canvas = this.signatureRef.el;
const img = this.getSVGStamp(font, stamp, canvas.width, canvas.height);
await this.printImage(img);
} else {
super.drawCurrentName();
}
},

getStampDetails() {
return {
name: this.props.signature.name,
company: this.props.signature.company,
address: this.props.signature.address,
city: this.props.signature.city,
country: this.props.signature.country,
vat: this.props.signature.vat,
image: this.props.signature.image,
};
},

getSVGStamp(font, stampData, width, height) {
const svg = renderToString("stamp_sign.sign_svg_stamp", {
width: width,
height: height,
font: font,
name: stampData.name,
company: stampData.company,
address: stampData.address,
city: stampData.city,
country: stampData.country,
vat: stampData.vat,
image: stampData.image,
});
return "data:image/svg+xml," + encodeURI(svg);
},
});
41 changes: 41 additions & 0 deletions sign_stamp/static/src/dialogs/name_and_sing.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="stamp_template" xml:space="preserve">
<t t-name="stamp_sign.sign_svg_stamp" name="SVG Stamp Text">
<svg t-att-width="width" t-att-height="height" viewBox="0 0 300 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
@font-face {
font-family: "font";
src: url(data:font/ttf;base64,<t t-esc="font"/>) format("woff");
font-weight: normal;
font-style: normal;
}
</style>
</defs>

<t t-if="company">
<text t-esc="company" x="150" y="20" font-size="14"/>
</t>

<t t-if="address">
<text t-esc="address" x="150" y="35" font-size="12"/>
</t>

<t t-if="city">
<text t-esc="city" x="150" y="50" font-size="12"/>
</t>

<t t-if="country">
<text t-esc="country" x="150" y="65" font-size="12"/>
</t>

<t t-if="vat">
<text t-esc="vat" x="150" y="80" font-size="10"/>
</t>

<t t-if="image">
<image x="30" y="5" width="100" height="100" t-att-href="image"/>
</t>
</svg>
</t>
</templates>
46 changes: 46 additions & 0 deletions sign_stamp/static/src/dialogs/stamp_dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Dialog } from "@web/core/dialog/dialog";
import { SignNameAndSignature, SignNameAndSignatureDialog } from "@sign/dialogs/sign_name_and_signature_dialog";

export class StampSignDetails extends SignNameAndSignature {
static template = "stamp_sign.StampSignDetails";

triggerFileUpload() {
const fileInput = document.querySelector("input[name='logo']");
if (fileInput) {
fileInput.click();
}
}

onInputStampDetails(ev) {
const field = ev.target.name;
if (field === "logo") {
const file = ev.target.files[0];
if (!file)
return;
const reader = new FileReader();
reader.onload = (e) => {
this.props.signature.image = e.target.result;
this.drawCurrentName();
};
reader.readAsDataURL(file);
return;
}
const value = ev.target.value;
if (field && this.props.signature?.hasOwnProperty(field)) {
this.props.signature[field] = value;
this.drawCurrentName();
}
}
}

export class StampSignDetailsDialog extends SignNameAndSignatureDialog {
static template = "stamp_sign.StampSignDetailsDialog";

static components = { Dialog, StampSignDetails };

get dialogProps() {
return {
title: "Adopt Your Stamp",
};
}
}
51 changes: 51 additions & 0 deletions sign_stamp/static/src/dialogs/stamp_dialog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0"?>
<templates>
<t t-name="stamp_sign.StampSignDetailsDialog">
<Dialog t-props="dialogProps">
<StampSignDetails t-props="nameAndSignatureProps" />
<div class="mt16 small">
By clicking Sign, I confirm this stamp will be used as my electronic signature.
</div>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="props.onConfirm" t-att-disabled="footerState.buttonsDisabled">Sign</button>
<button class="btn btn-secondary" t-on-click="props.close">Cancel</button>
</t>
</Dialog>
</t>

<t t-name="stamp_sign.StampSignDetails" t-inherit="sign.NameAndSignature" t-inherit-mode="primary">
<xpath expr="//div[@t-if='!props.noInputName']" position="replace">
<div t-if="!props.noInputName" class="o_web_sign_name_group">
<label class="col-form-label">Full Name</label>
<input type="text" name="signer" class="o_web_sign_name_input form-control" t-on-input="onInputSignName" t-att-value="props.signature.name" placeholder="Type your name"/>
</div>
<div t-if="!props.noInputName" class="o_web_sign_name_group">
<label class="col-form-label">Company</label>
<input type="text" name="company" class="o_web_sign_name_input form-control" t-on-input="onInputStampDetails" t-att-value="props.signature.company" placeholder="Type your company name"/>
</div>
<div t-if="!props.noInputName" class="o_web_sign_name_group">
<label class="col-form-label">Address</label>
<input type="text" name="address" class="o_web_sign_name_input form-control form-control" t-on-input="onInputStampDetails" t-att-value="props.signature.address" placeholder="Type your Address"/>
</div>
<div t-if="!props.noInputName" class="o_web_sign_name_group">
<label class="col-form-label">City</label>
<input type="text" name="city" class="o_web_sign_name_input form-control form-control" t-on-input="onInputStampDetails" t-att-value="props.signature.city" placeholder="Type your city name"/>
</div>
<div t-if="!props.noInputName" class="o_web_sign_name_group">
<label class="col-form-label">Country</label>
<input type="text" name="country" class="o_web_sign_name_input form-control form-control" t-on-input="onInputStampDetails" t-att-value="props.signature.country" placeholder="Type your country name"/>
</div>
<div t-if="!props.noInputName" class="o_web_sign_name_group">
<label class="col-form-label">VAT Number</label>
<input type="text" name="vat" class="o_web_sign_name_input form-control form-control" t-on-input="onInputStampDetails" t-att-value="props.signature.vat" placeholder="Type your VAT number"/>
</div>
<div t-if="!props.noInputName" class="o_web_sign_name_group d-flex align-items-center gap-3 mt-2">
<label class="col-form-label">Logo</label>
<div class="upload-button">
<input type="file" name="logo" class="d-none" t-on-input="onInputStampDetails" accept="image/*"/>
<button type="button" class="btn btn-primary" t-on-click="triggerFileUpload">Upload</button>
</div>
</div>
</xpath>
</t>
</templates>
12 changes: 12 additions & 0 deletions sign_stamp/views/sign_request_templates.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<odoo>
<template id="sign_stamp._doc_sign" inherit_id="sign._doc_sign">
<xpath expr="//input[@id='o_sign_signer_phone_input_info']" position="before">
<input id="o_sign_signer_company_input_info" type="hidden" t-att-value="current_request_item.partner_id.company_id.name if current_request_item and current_request_item.partner_id else None"/>
<input id="o_sign_signer_address_input_info" type="hidden" t-att-value="current_request_item.partner_id.company_id.partner_id.street if current_request_item and current_request_item.partner_id else None"/>
<input id="o_sign_signer_city_input_info" type="hidden" t-att-value="current_request_item.partner_id.company_id.partner_id.city if current_request_item and current_request_item.partner_id else None"/>
<input id="o_sign_signer_country_input_info" type="hidden" t-att-value="current_request_item.partner_id.company_id.partner_id.country_id.name if current_request_item and current_request_item.partner_id else None"/>
<input id="o_sign_signer_vat_input_info" type="hidden" t-att-value="current_request_item.partner_id.company_id.partner_id.vat if current_request_item and current_request_item.partner_id else None"/>
</xpath>
</template>
</odoo>