Skip to content

[ADD] stamp_sign: add company stamp field #909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
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 awesome_gallery/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'version': '0.1',
'application': True,
'category': 'Tutorials/AwesomeGallery',
'author': 'arkp',
'installable': True,
'depends': ['web', 'contacts'],
'data': [
Expand Down
1 change: 1 addition & 0 deletions awesome_kanban/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'version': '0.1',
'application': True,
'category': 'Tutorials/AwesomeKanban',
'author': 'arkp',
'installable': True,
'depends': ['web', 'crm'],
'data': [
Expand Down
4 changes: 4 additions & 0 deletions stamp_sign/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
from . import controllers
26 changes: 26 additions & 0 deletions stamp_sign/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
"name": "Stamp Sign",
"version": "1.0",
"depends": ["sign"],
"category": "Sign",
"author": "arkp",
"data": [
"data/sign_data.xml",
"views/sign_request_templates.xml",
],
"assets": {
"web.assets_backend": [
"stamp_sign/static/src/components/sign_request/*",
"stamp_sign/static/src/dialogs/*",
],
"sign.assets_public_sign": [
"stamp_sign/static/src/components/sign_request/*",
"stamp_sign/static/src/dialogs/*",
],
},
"installable": True,
"application": True,
"license": "LGPL-3",
}
3 changes: 3 additions & 0 deletions stamp_sign/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import main
51 changes: 51 additions & 0 deletions stamp_sign/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import http
from odoo.addons.sign.controllers.main import Sign # type: ignore


class Sign(Sign):
def get_document_qweb_context(self, sign_request_id, token, **post):
data = super().get_document_qweb_context(sign_request_id, token, **post)
company_logo = http.request.env.user.company_id.logo
if company_logo:
data["logo"] = "data:image/png;base64,%s" % company_logo.decode()
else:
data["logo"] = False

return data

@http.route(["/sign/update_user_signature"], type="json", auth="user")
def update_signature(
self, sign_request_id, role, signature_type=None, datas=None, frame_datas=None
):
user = http.request.env.user
if not user or signature_type not in [
"sign_signature",
"sign_initials",
]:
return False

sign_request_item_sudo = (
http.request.env["sign.request.item"]
.sudo()
.search(
[("sign_request_id", "=", sign_request_id), ("role_id", "=", role)],
limit=1,
)
)

allowed = sign_request_item_sudo.partner_id.id == user.partner_id.id
if not allowed:
return False
if datas:
user[signature_type] = datas[datas.find(",") + 1 :]
else:
user[signature_type] = False

if frame_datas:
user[signature_type + "_frame"] = frame_datas[frame_datas.find(",") + 1 :]
else:
user[signature_type + "_frame"] = False

return True
12 changes: 12 additions & 0 deletions stamp_sign/data/sign_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record model="sign.item.type" id="stamp_item_type">
<field name="name">Stamp</field>
<field name="item_type">stamp</field>
<field name="tip">stamp</field>
<field name="placeholder">Stamp</field>
<field name="default_width" type="float">0.300</field>
<field name="default_height" type="float">0.10</field>
<field name="icon">fa-legal</field>
</record>
</odoo>
4 changes: 4 additions & 0 deletions stamp_sign/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import sign_template
from . import sign_request
228 changes: 228 additions & 0 deletions stamp_sign/models/sign_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import base64
import io
import time
from PIL import UnidentifiedImageError
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import Paragraph
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

from odoo import _, models
from odoo.tools import format_date, is_html_empty
from odoo.exceptions import UserError, ValidationError
from odoo.tools.pdf import PdfFileReader, PdfFileWriter
from odoo.tools.misc import get_lang

try:
from PyPDF2.errors import PdfReadError # type: ignore
except ImportError:
try:
from PyPDF2.utils import PdfReadError
except ImportError:
PdfReadError = Exception


def _fix_image_transparency(image):
pixels = image.load()
for x in range(image.size[0]):
for y in range(image.size[1]):
if pixels[x, y] == (0, 0, 0, 0):
pixels[x, y] = (255, 255, 255, 0)


class SignRequest(models.Model):
_inherit = "sign.request"

def _generate_completed_document(self, password=""):
self.ensure_one()
if self.state != 'signed':
raise UserError(_("The completed document cannot be created because the sign request is not fully signed"))
if not self.template_id.sign_item_ids:
self.completed_document = self.template_id.attachment_id.datas
else:
try:
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
old_pdf.getNumPages()
except Exception:
raise ValidationError(_("ERROR: Invalid PDF file!"))

isEncrypted = old_pdf.isEncrypted
if isEncrypted and not old_pdf.decrypt(password):
return

font = self._get_font()
normalFontSize = self._get_normal_font_size()

packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=self.get_page_size(old_pdf))
itemsByPage = self.template_id._get_sign_items_by_page()
items_ids = [id for items in itemsByPage.values() for id in items.ids]
values_dict = self.env['sign.request.item.value']._read_group(
[('sign_item_id', 'in', items_ids), ('sign_request_id', '=', self.id)],
groupby=['sign_item_id'],
aggregates=['value:array_agg', 'frame_value:array_agg', 'frame_has_hash:array_agg']
)
values = {
sign_item.id : {
'value': values[0],
'frame': frame_values[0],
'frame_has_hash': frame_has_hashes[0],
}
for sign_item, values, frame_values, frame_has_hashes in values_dict
}

for p in range(0, old_pdf.getNumPages()):
page = old_pdf.getPage(p)
width = float(abs(page.mediaBox.getWidth()))
height = float(abs(page.mediaBox.getHeight()))

rotation = page.get('/Rotate', 0)
if rotation and isinstance(rotation, int):
can.rotate(rotation)
if rotation == 90:
width, height = height, width
can.translate(0, -height)
elif rotation == 180:
can.translate(-width, -height)
elif rotation == 270:
width, height = height, width
can.translate(-width, 0)

items = itemsByPage[p + 1] if p + 1 in itemsByPage else []
for item in items:
value_dict = values.get(item.id)
if not value_dict:
continue
value = value_dict['value']
frame = value_dict['frame']

if frame:
try:
image_reader = ImageReader(io.BytesIO(base64.b64decode(frame[frame.find(',')+1:])))
except UnidentifiedImageError:
raise ValidationError(_("There was an issue downloading your document. Please contact an administrator."))
_fix_image_transparency(image_reader._image)
can.drawImage(
image_reader,
width*item.posX,
height*(1-item.posY-item.height),
width*item.width,
height*item.height,
'auto',
True
)

if item.type_id.item_type == "text":
can.setFont(font, height*item.height*0.8)
if item.alignment == "left":
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
elif item.alignment == "right":
can.drawRightString(width*(item.posX+item.width), height*(1-item.posY-item.height*0.9), value)
else:
can.drawCentredString(width*(item.posX+item.width/2), height*(1-item.posY-item.height*0.9), value)

elif item.type_id.item_type == "selection":
content = []
for option in item.option_ids:
if option.id != int(value):
content.append("<strike>%s</strike>" % (option.value))
else:
content.append(option.value)
font_size = height * normalFontSize * 0.8
text = " / ".join(content)
string_width = stringWidth(text.replace("<strike>", "").replace("</strike>", ""), font, font_size)
p = Paragraph(text, ParagraphStyle(name='Selection Paragraph', fontName=font, fontSize=font_size, leading=12))
posX = width * (item.posX + item.width * 0.5) - string_width // 2
posY = height * (1 - item.posY - item.height * 0.5) - p.wrap(width, height)[1] // 2
p.drawOn(can, posX, posY)

elif item.type_id.item_type == "textarea":
font_size = height * normalFontSize * 0.8
can.setFont(font, font_size)
lines = value.split('\n')
y = (1-item.posY)
for line in lines:
empty_space = width * item.width - can.stringWidth(line, font, font_size)
x_shift = 0
if item.alignment == 'center':
x_shift = empty_space / 2
elif item.alignment == 'right':
x_shift = empty_space
y -= normalFontSize * 0.9
can.drawString(width * item.posX + x_shift, height * y, line)
y -= normalFontSize * 0.1

elif item.type_id.item_type == "checkbox":
can.setFont(font, height*item.height*0.8)
value = 'X' if value == 'on' else ''
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
elif item.type_id.item_type == "radio":
x = width * item.posX
y = height * (1 - item.posY)
w = item.width * width
h = item.height * height
c_x = x + w * 0.5
c_y = y - h * 0.5
can.circle(c_x, c_y, h * 0.5)
if value == "on":
can.circle(x_cen=c_x, y_cen=c_y, r=h * 0.5 * 0.75, fill=1)
elif item.type_id.item_type in ["signature", "initial", "stamp"]:
try:
image_reader = ImageReader(io.BytesIO(base64.b64decode(value[value.find(',')+1:])))
except UnidentifiedImageError:
raise ValidationError(_("There was an issue downloading your document. Please contact an administrator."))
_fix_image_transparency(image_reader._image)
can.drawImage(image_reader, width*item.posX, height*(1-item.posY-item.height), width*item.width, height*item.height, 'auto', True)

can.showPage()

can.save()

item_pdf = PdfFileReader(packet, overwriteWarnings=False)
new_pdf = PdfFileWriter()

for p in range(0, old_pdf.getNumPages()):
page = old_pdf.getPage(p)
page.mergePage(item_pdf.getPage(p))
new_pdf.addPage(page)

if isEncrypted:
new_pdf.encrypt(password)

try:
output = io.BytesIO()
new_pdf.write(output)
except PdfReadError:
raise ValidationError(_("There was an issue downloading your document. Please contact an administrator."))

self.completed_document = base64.b64encode(output.getvalue())
output.close()

attachment = self.env['ir.attachment'].create({
'name': "%s.pdf" % self.reference if self.reference.split('.')[-1] != 'pdf' else self.reference,
'datas': self.completed_document,
'type': 'binary',
'res_model': self._name,
'res_id': self.id,
})
public_user = self.env.ref('base.public_user', raise_if_not_found=False)
if not public_user:
public_user = self.env.user
pdf_content, __ = self.env["ir.actions.report"].with_user(public_user).sudo()._render_qweb_pdf(
'sign.action_sign_request_print_logs',
self.ids,
data={'format_date': format_date, 'company_id': self.communication_company_id}
)
attachment_log = self.env['ir.attachment'].create({
'name': "Certificate of completion - %s.pdf" % time.strftime('%Y-%m-%d - %H:%M:%S'),
'raw': pdf_content,
'type': 'binary',
'res_model': self._name,
'res_id': self.id,
})
self.completed_document_attachment_ids = [(6, 0, [attachment.id, attachment_log.id])]
9 changes: 9 additions & 0 deletions stamp_sign/models/sign_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models


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

item_type = fields.Selection(selection_add=[("stamp", "Stamp")], ondelete={"stamp": "set default"})
23 changes: 23 additions & 0 deletions stamp_sign/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;
const fields = ["company", "address", "city", "country", "vat", "logo",];
this.signerInfo = {};

for (const field of fields) {
const element = parentEl.querySelector(`#o_sign_signer_${field}_input_info`);
this.signerInfo[field] = element?.value;
}
},

get iframeProps() {
return {
...super.iframeProps,
...this.signerInfo,
};
},
});
Loading