Skip to content

[ADD] sign_stamp: add custom Stamp field in Sign app #911

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
2 changes: 2 additions & 0 deletions sign_stamp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import controllers
23 changes: 23 additions & 0 deletions sign_stamp/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "Sign Stamp",
"version": "1.0",
"depends": ["sign"],
"category": "Sign",
"data": [
"data/sign_data.xml",
"views/sign_request_templates.xml",
],
"assets": {
"web.assets_backend": [
"sign_stamp/static/src/components/sign_request/*",
"sign_stamp/static/src/dialogs/*",
],
"sign.assets_public_sign": [
"sign_stamp/static/src/components/sign_request/*",
"sign_stamp/static/src/dialogs/*",
],
},
"installable": True,
"application": True,
"license": "OEEL-1",
}
1 change: 1 addition & 0 deletions sign_stamp/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
73 changes: 73 additions & 0 deletions sign_stamp/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from odoo import http
from odoo.addons.sign.controllers.main import Sign


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)
current_request_item = data["current_request_item"]
sign_item_types = data["sign_item_types"]
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

if current_request_item:
user_stamp = current_request_item._get_user_signature_asset("stamp_sign_stamp")
user_stamp_frame = current_request_item._get_user_signature_asset("stamp_sign_stamp_frame")

encoded_user_stamp = (
"data:image/png;base64,%s" % user_stamp.decode()
if user_stamp
else False
)
encoded_user_stamp_frame = (
"data:image/png;base64,%s" % user_stamp_frame.decode()
if user_stamp_frame
else False
)

stamp_item_type = next(
(
item_type
for item_type in sign_item_types
if item_type["item_type"] == "stamp"
),
None,
)

if stamp_item_type:
stamp_item_type["auto_value"] = encoded_user_stamp
stamp_item_type["frame_value"] = encoded_user_stamp_frame

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",
"stamp_sign_stamp",
]:
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
user[signature_type] = datas[datas.find(",") + 1 :]
if frame_datas:
user[signature_type + "_frame"] = frame_datas[frame_datas.find(",") + 1 :]
return True
12 changes: 12 additions & 0 deletions sign_stamp/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-certificate</field>
</record>
</odoo>
3 changes: 3 additions & 0 deletions sign_stamp/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import sign_template
from . import sign_request
from . import res_user
22 changes: 22 additions & 0 deletions sign_stamp/models/res_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from odoo import models, fields

SIGN_USER = ["sign_stamp"]


class ResUsers(models.Model):
_inherit = "res.users"

@property
def readable_field(self):
return super().readable_field + SIGN_USER

@property
def writeable_field(self):
return super().writeable_field + SIGN_USER

stamp_sign_stamp = fields.Binary(
string="Company Stamp", copy=False, groups="base.group_user"
)
stamp_sign_stamp_frame = fields.Binary(
string="Company Stamp Frame", copy=False, groups="base.group_user"
)
244 changes: 244 additions & 0 deletions sign_stamp/models/sign_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import base64
import io
import time

from PIL import UnidentifiedImageError
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas

from odoo import _, models, Command
from odoo.tools import format_date
from odoo.exceptions import UserError, ValidationError
from odoo.tools.pdf import PdfFileReader, PdfFileWriter, PdfReadError


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()
self._validate_document_state()

if not self.template_id.sign_item_ids:
self._copy_template_to_completed_document()
else:
old_pdf = self._load_template_pdf(password)
new_pdf_data = self._create_signed_overlay(old_pdf)
self._merge_pdfs_and_store(old_pdf, new_pdf_data, password)

attachment = self._create_attachment_from_completed_doc()
log_attachment = self._create_completion_certificate()
self._attach_completed_documents(attachment, log_attachment)

def _validate_document_state(self):
if self.state != "signed":
raise UserError(
_(
"The completed document cannot be created"
)
)

def _copy_template_to_completed_document(self):
self.completed_document = self.template_id.attachment_id.datas

def _load_template_pdf(self, password):
try:
pdf_reader = PdfFileReader(
io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)),
strict=False,
overwriteWarnings=False,
)
pdf_reader.getNumPages()
except PdfReadError:
raise ValidationError(_("ERROR: Invalid PDF file!"))

if pdf_reader.isEncrypted and not pdf_reader.decrypt(password):
return

return pdf_reader

def _create_signed_overlay(self, old_pdf):
font = self._get_font()
normalFontSize = self._get_normal_font_size()
packet = io.BytesIO()
can = canvas.Canvas(packet, pagesize=self.get_page_size(old_pdf))
items_by_page, values = self._collect_items_and_values()

for p in range(0, old_pdf.getNumPages()):
page = old_pdf.getPage(p)
width, height = self._get_page_dimensions(page)
self._apply_page_rotation(can, page, width, height)

for item in items_by_page.get(p + 1, []):
self._draw_item(
can, item, values.get(item.id), width, height, font, normalFontSize
)
can.showPage()

can.save()
return PdfFileReader(packet, overwriteWarnings=False)

def _collect_items_and_values(self):
items_by_page = self.template_id._get_sign_items_by_page()
item_ids = [id for items in items_by_page.values() for id in items.ids]
values_dict = self.env["sign.request.item.value"]._read_group(
[("sign_item_id", "in", item_ids), ("sign_request_id", "=", self.id)],
groupby=["sign_item_id"],
aggregates=[
"value:array_agg",
"frame_value:array_agg",
"frame_has_hash:array_agg",
],
)
values = {
item: {"value": vals[0], "frame": frames[0], "frame_has_hash": hashes[0]}
for item, vals, frames, hashes in values_dict
}
return items_by_page, values

def _get_page_dimensions(self, page):
width = float(abs(page.mediaBox.getWidth()))
height = float(abs(page.mediaBox.getHeight()))
return width, height

def _apply_page_rotation(self, can, page, width, height):
rotation = page.get("/Rotate", 0)
if 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)

def _draw_item(self, can, item, value_dict, width, height, font, normalFontSize):
if not value_dict:
return

value, frame = value_dict["value"], value_dict["frame"]
if frame:
self._draw_image(can, frame, item, width, height)

draw_method = getattr(self, f"_draw_{item.type_id.item_type}", None)
if draw_method:
draw_method(can, item, value, width, height, font, normalFontSize)

def _draw_image(self, can, frame_data, item, width, height):
try:
image_reader = ImageReader(
io.BytesIO(base64.b64decode(frame_data.split(",")[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,
)

def _draw_signature(self, can, item, value, width, height, *_):
self._draw_image(can, value, item, width, height)

_draw_initial = _draw_signature
_draw_stamp = _draw_signature

def _merge_pdfs_and_store(self, old_pdf, overlay_pdf, password):
new_pdf = PdfFileWriter()
for i in range(old_pdf.getNumPages()):
page = old_pdf.getPage(i)
page.mergePage(overlay_pdf.getPage(i))
new_pdf.addPage(page)
if old_pdf.isEncrypted:
new_pdf.encrypt(password)

output = io.BytesIO()
try:
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()

def _create_attachment_from_completed_doc(self):
filename = (
self.reference
if self.reference.endswith(".pdf")
else f"{self.reference}.pdf"
)
return self.env["ir.attachment"].create(
{
"name": filename,
"datas": self.completed_document,
"type": "binary",
"res_model": self._name,
"res_id": self.id,
}
)

def _create_completion_certificate(self):
public_user = (
self.env.ref("base.public_user", raise_if_not_found=False) or 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,
},
)
)
return self.env["ir.attachment"].create(
{
"name": f"Certificate of completion - {time.strftime('%Y-%m-%d - %H:%M:%S')}.pdf",
"raw": pdf_content,
"type": "binary",
"res_model": self._name,
"res_id": self.id,
}
)

def _attach_completed_documents(self, doc_attachment, log_attachment):
self.completed_document_attachment_ids = [
Command.set([doc_attachment.id, log_attachment.id])
]


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

def _get_user_signature_asset(self, asset_type):
self.ensure_one()
sign_user = self.partner_id.user_ids[:1]
if sign_user and asset_type in ["stamp_sign_stamp", "stamp_sign_stamp_frame"]:
return sign_user[asset_type]
return False
10 changes: 10 additions & 0 deletions sign_stamp/models/sign_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from odoo import models, fields


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

item_type = fields.Selection(
selection_add=[('stamp', 'Stamp')],
ondelete={'stamp': 'cascade'}
)
Loading