Browse Source

report_qweb_signer addon

pull/253/head
Antonio Espinosa 8 years ago
committed by Pedro M. Baeza
parent
commit
df96442b96
  1. 116
      report_qweb_signer/README.rst
  2. 5
      report_qweb_signer/__init__.py
  3. 31
      report_qweb_signer/__openerp__.py
  4. 21
      report_qweb_signer/demo/report_certificate.xml
  5. 46
      report_qweb_signer/demo/report_partner.xml
  6. 161
      report_qweb_signer/i18n/es.po
  7. 7
      report_qweb_signer/models/__init__.py
  8. 193
      report_qweb_signer/models/report.py
  9. 42
      report_qweb_signer/models/report_certificate.py
  10. 14
      report_qweb_signer/models/res_company.py
  11. 3
      report_qweb_signer/security/ir.model.access.csv
  12. BIN
      report_qweb_signer/static/certificate/test.p12
  13. 1
      report_qweb_signer/static/certificate/test.passwd
  14. BIN
      report_qweb_signer/static/description/icon.png
  15. 1
      report_qweb_signer/static/description/noun_65694_cc.svg
  16. BIN
      report_qweb_signer/static/jar/itext-1.4.8.jar
  17. BIN
      report_qweb_signer/static/jar/jPdfSign.jar
  18. 333
      report_qweb_signer/static/src/java/JPdfSign.java
  19. 3
      report_qweb_signer/static/src/java/strings.properties
  20. 66
      report_qweb_signer/views/report_certificate_view.xml
  21. 26
      report_qweb_signer/views/res_company_view.xml

116
report_qweb_signer/README.rst

@ -0,0 +1,116 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
=======================
Qweb PDF reports signer
=======================
This module extends the functionality of report module to sign
PDFs using a PKCS#12 certificate.
Installation
============
To install this module, you need to install Java JDK::
apt-get install openjdk-7-jre-headless
Configuration
=============
In order to start signing PDF documents you need to configure certificate(s)
to use in your company.
* Go to ``Settings > Companies > Companies > Your company``
* Go to ``Report configuration`` tab
* Click ``Edit``
* Add a new item in ``PDF report certificates`` list
* Click ``Create``
* Set name, certificate file, password file and model
* Optionally you can set a domain and filename pattern for saving as attachment
For example, if you want to sign only customer invoices in open or paid state:
* Model: ``account.invoice``
* Domain: ``[('type','=','out_invoice'), ('state', 'in', ('open', 'paid'))]``
* Save as attachment: ``(object.number or '').replace('/','_') + '.signed.pdf'``
**Note**: Linux user that executes Odoo server process must have
read access to certificate file and password file
Usage
=====
User just prints PDF documents (only Qweb PDF reports supported) as usual,
but signed PDF is automatically downloaded if this document model is configured
as indicated above.
If 'Save as attachment' is configured, signed PDF is saved as attachment and
next time saved one is downloaded without signing again. This is appropiate when
signing date is important, for example, when signing customer invoices.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/143/8.0
For further information, please visit:
* https://www.odoo.com/forum/help-1
Known issues / Roadmap
======================
* When signing multiple documents (if 'Allow only one document' is disable)
then 'Save as attachment' is not applied and signed result is not
saved as attachment.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/reporting-engine/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/reporting-engine/issues/new?body=module:%20report_qweb_signer%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
External utilities
------------------
* iText v1.4.8: © 2000-2006, Paulo Soares, Bruno Lowagie and others - License `MPL <http://www.mozilla.org/MPL>`_ or `LGPL2 <http://www.gnu.org/licenses/old-licenses/lgpl-2.0.html>`_ - http://sourceforge.net/projects/itext
* jPdfSign: © 2006 Jan Peter Stotz - License `MPL <http://www.mozilla.org/MPL>`_ or `LGPL2 <http://www.gnu.org/licenses/old-licenses/lgpl-2.0.html>`_ (inherited from iText) - http://private.sit.fraunhofer.de/~stotz/software/jpdfsign
* Modified jPdfSign: © 2015 Antonio Espinosa - License `MPL <http://www.mozilla.org/MPL>`_ or `LGPL2 <http://www.gnu.org/licenses/old-licenses/lgpl-2.0.html>`_ (inherited from iText) - static/src/java/JPdfSign.java
Icon
----
`Created by Anton Noskov from the Noun Project <https://thenounproject.com/search/?q=signed+contract&i=65694>`_
Contributors
------------
* Rafael Blasco <rafabn@antiun.com>
* Antonio Espinosa <antonioea@antiun.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.

5
report_qweb_signer/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models

31
report_qweb_signer/__openerp__.py

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Qweb PDF reports signer",
"summary": "Sign Qweb PDFs usign a PKCS#12 certificate",
"version": "8.0.1.0.0",
"category": "Reporting",
"website": "http://www.antiun.com",
"author": "Antiun Ingeniería S.L., "
"Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"report",
],
"external_dependencies": {
"bin": ['/usr/bin/java'],
},
"data": [
"security/ir.model.access.csv",
"views/report_certificate_view.xml",
"views/res_company_view.xml",
],
"demo": [
"demo/report_partner.xml",
"demo/report_certificate.xml",
],
}

21
report_qweb_signer/demo/report_certificate.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015 Antiun Ingenieria S.L. - Antonio Espinosa
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<data noupdate="1">
<record id="demo_certificate_test" model="report.certificate">
<field name="company_id" ref="base.main_company"/>
<field name="name">Test OCA certificate</field>
<field name="path">test.p12</field>
<field name="password_file">test.passwd</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="domain">[('customer', '=', True)]</field>
<field name="allow_only_one" eval="True"/>
<field name="attachment">'test_' + (object.name or '').replace(' ', '_').lower() + '.signed.pdf'</field>
</record>
</data>
</openerp>

46
report_qweb_signer/demo/report_partner.xml

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015 Antiun Ingenieria S.L. - Antonio Espinosa
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<data>
<template id="report_partner_demo_document">
<t t-call="report.external_layout">
<div class="page">
<div class="row">
<div class="col-md-12">
This is a sample report for testing PDF certificates
</div>
</div>
<div class="row">
<div class="col-md-12">
<strong>Partner:</strong> <span t-field="o.name"/>
</div>
</div>
</div>
</t>
</template>
<template id="report_partner_demo">
<t t-call="report.html_container">
<t t-foreach="doc_ids" t-as="doc_id">
<t t-raw="translate_doc(doc_id, doc_model, 'lang', 'report_qweb_signer.report_partner_demo_document')"/>
</t>
</t>
</template>
<report
id="partner_demo"
model="res.partner"
string="Test PDF certificate"
report_type="qweb-pdf"
name="report_qweb_signer.report_partner_demo"
file="report_qweb_signer.report_partner_demo"
attachment_use="True"
attachment="'test_' + (object.name or '').replace(' ', '_').lower() + '.pdf'"
/>
</data>
</openerp>

161
report_qweb_signer/i18n/es.po

@ -0,0 +1,161 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * report_qweb_signer
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-11-22 19:28+0000\n"
"PO-Revision-Date: 2015-11-22 19:28+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: report_qweb_signer
#: field:report.certificate,allow_only_one:0
msgid "Allow only one document"
msgstr "Sólo un documento"
#. module: report_qweb_signer
#: field:report.certificate,path:0
msgid "Certificate file path"
msgstr "Ruta al certificado"
#. module: report_qweb_signer
#: view:res.company:report_qweb_signer.view_company_form
msgid "Certificates"
msgstr "Certificados"
#. module: report_qweb_signer
#: model:ir.model,name:report_qweb_signer.model_res_company
msgid "Companies"
msgstr "Compañías"
#. module: report_qweb_signer
#: field:report.certificate,company_id:0
msgid "Company"
msgstr "Compañía"
#. module: report_qweb_signer
#: field:report.certificate,create_uid:0
msgid "Created by"
msgstr "Creado por"
#. module: report_qweb_signer
#: field:report.certificate,create_date:0
msgid "Created on"
msgstr "Creado en"
#. module: report_qweb_signer
#: field:report.certificate,domain:0
msgid "Domain"
msgstr "Dominio"
#. module: report_qweb_signer
#: help:report.certificate,domain:0
msgid "Domain for filtering if sign or not the document"
msgstr "Dominio para filrar si firmar o no el documento"
#. module: report_qweb_signer
#: help:report.certificate,attachment:0
msgid "Filename used to store signed document as attachment. Keep empty to not save signed document."
msgstr "Nombre de fichero usado para guardar el documento firmado como adjunto. Dejar en blanco para no guardar el documento firmado."
#. module: report_qweb_signer
#: field:report.certificate,id:0
msgid "ID"
msgstr "ID"
#. module: report_qweb_signer
#: help:report.certificate,allow_only_one:0
msgid "If True, this certificate can not be used to sign a PDF from several documents."
msgstr "Si está activo, este certificado no puede usarse para firmar un PDF de varios documentos."
#. module: report_qweb_signer
#: field:report.certificate,write_uid:0
msgid "Last Updated by"
msgstr "Última actualización por"
#. module: report_qweb_signer
#: field:report.certificate,write_date:0
msgid "Last Updated on"
msgstr "Última actualización en"
#. module: report_qweb_signer
#: field:report.certificate,model_id:0
msgid "Model"
msgstr "Modelo"
#. module: report_qweb_signer
#: help:report.certificate,model_id:0
msgid "Model where apply this certificate"
msgstr "Modelo en el que usar este certificado para firmar"
#. module: report_qweb_signer
#: field:report.certificate,name:0
msgid "Name"
msgstr "Nombre"
#. module: report_qweb_signer
#: model:ir.actions.act_window,name:report_qweb_signer.action_report_certificate
#: model:ir.ui.menu,name:report_qweb_signer.menu_report_certificate
msgid "PDF certificates"
msgstr "Certificados PDF"
#. module: report_qweb_signer
#: view:report.certificate:report_qweb_signer.view_report_certificate_form
msgid "PDF report certificate"
msgstr "Certificado de informe PDF"
#. module: report_qweb_signer
#: view:report.certificate:report_qweb_signer.view_report_certificate_tree
#: field:res.company,report_certificate_ids:0
msgid "PDF report certificates"
msgstr "Certificados de informes PDF"
#. module: report_qweb_signer
#: field:report.certificate,password_file:0
msgid "Password file path"
msgstr "Ruta al fichero de contraseña"
#. module: report_qweb_signer
#: help:report.certificate,path:0
msgid "Path to PKCS#12 certificate file"
msgstr "Ruta al fichero de certificado PKCS#12"
#. module: report_qweb_signer
#: help:report.certificate,password_file:0
msgid "Path to certificate password file"
msgstr "Ruta al fichero que contiene la contraseña con la que se proteje el fichero de certificado"
#. module: report_qweb_signer
#: code:addons/report_qweb_signer/models/report.py:77
#, python-format
msgid "PortableSigner failed (error code: %s). Message: %s"
msgstr "PortableSigner falló (código de error: %s). Mensaje: %s"
#. module: report_qweb_signer
#: model:ir.model,name:report_qweb_signer.model_report
msgid "Report"
msgstr "Informe"
#. module: report_qweb_signer
#: field:report.certificate,attachment:0
msgid "Save as attachment"
msgstr "Salvar como adjunto"
#. module: report_qweb_signer
#: field:report.certificate,sequence:0
msgid "Sequence"
msgstr "Secuencia"
#. module: report_qweb_signer
#: code:addons/report_qweb_signer/models/report.py:76
#, python-format
msgid "Signing report (PDF)"
msgstr "Firmando informe (PDF)"

7
report_qweb_signer/models/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import report
from . import report_certificate
from . import res_company

193
report_qweb_signer/models/report.py

@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import base64
from contextlib import closing
import os
import subprocess
import tempfile
import time
from openerp import models, api, _
from openerp.exceptions import Warning, AccessError
from openerp.tools.safe_eval import safe_eval
import logging
_logger = logging.getLogger(__name__)
def _normalize_filepath(path):
path = path or ''
path = path.strip()
if not os.path.isabs(path):
me = os.path.dirname(__file__)
path = '{}/../static/certificate/'.format(me) + path
path = os.path.normpath(path)
return path if os.path.exists(path) else False
class Report(models.Model):
_inherit = 'report'
def _certificate_get(self, cr, uid, ids, report, context=None):
if report.report_type != 'qweb-pdf':
_logger.info(
"Can only sign qweb-pdf reports, this one is '%s' type",
report.report_type)
return False
m_cert = self.pool['report.certificate']
company_id = self.pool['res.users']._get_company(cr, uid)
certificate_ids = m_cert.search(cr, uid, [
('company_id', '=', company_id),
('model_id', '=', report.model)], context=context)
if not certificate_ids:
_logger.info(
"No PDF certificate found for report '%s'",
report.report_name)
return False
for cert in m_cert.browse(cr, uid, certificate_ids, context=context):
# Check allow only one document
if cert.allow_only_one and len(ids) > 1:
_logger.info(
"Certificate '%s' allows only one document, "
"but printing %d documents",
cert.name, len(ids))
continue
# Check domain
if cert.domain:
m_model = self.pool[cert.model_id.model]
domain = [('id', 'in', tuple(ids))]
domain = domain + safe_eval(cert.domain)
doc_ids = m_model.search(cr, uid, domain, context=context)
if not doc_ids:
_logger.info(
"Certificate '%s' domain not satisfied", cert.name)
continue
# Certificate match!
return cert
return False
def _attach_filename_get(self, cr, uid, ids, certificate, context=None):
if len(ids) != 1:
return False
obj = self.pool[certificate.model_id.model].browse(cr, uid, ids[0])
filename = safe_eval(certificate.attachment, {
'object': obj,
'time': time
})
return filename
def _attach_signed_read(self, cr, uid, ids, certificate, context=None):
if len(ids) != 1:
return False
filename = self._attach_filename_get(
cr, uid, ids, certificate, context=context)
if not filename:
return False
signed = False
m_attachment = self.pool['ir.attachment']
attach_ids = m_attachment.search(cr, uid, [
('datas_fname', '=', filename),
('res_model', '=', certificate.model_id.model),
('res_id', '=', ids[0])
])
if attach_ids:
signed = m_attachment.browse(cr, uid, attach_ids[0]).datas
signed = base64.decodestring(signed)
return signed
def _attach_signed_write(self, cr, uid, ids, certificate, signed,
context=None):
if len(ids) != 1:
return False
filename = self._attach_filename_get(
cr, uid, ids, certificate, context=context)
if not filename:
return False
m_attachment = self.pool['ir.attachment']
try:
attach_id = m_attachment.create(cr, uid, {
'name': filename,
'datas': base64.encodestring(signed),
'datas_fname': filename,
'res_model': certificate.model_id.model,
'res_id': ids[0],
})
except AccessError:
raise Warning(
_('Saving signed report (PDF): '
'You do not have enought access rights to save attachments'))
else:
_logger.info(
"The signed PDF document '%s' is now saved in the database",
filename)
return attach_id
def _signer_bin(self, opts):
me = os.path.dirname(__file__)
java_bin = 'java -jar -Xms4M -Xmx4M'
jar = '{}/../static/jar/jPdfSign.jar'.format(me)
return '%s %s %s' % (java_bin, jar, opts)
def pdf_sign(self, pdf, certificate):
pdfsigned = pdf + '.signed.pdf'
p12 = _normalize_filepath(certificate.path)
passwd = _normalize_filepath(certificate.password_file)
if not (p12 and passwd):
raise Warning(
_('Signing report (PDF): '
'Certificate or password file not found'))
signer_opts = '"%s" "%s" "%s" "%s"' % (p12, pdf, pdfsigned, passwd)
signer = self._signer_bin(signer_opts)
process = subprocess.Popen(
signer, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
out, err = process.communicate()
if process.returncode:
raise Warning(
_('Signing report (PDF): jPdfSign failed (error code: %s). '
'Message: %s. Output: %s') %
(process.returncode, err, out))
return pdfsigned
@api.v7
def get_pdf(self, cr, uid, ids, report_name, html=None, data=None,
context=None):
signed_content = False
report = self._get_report_from_name(cr, uid, report_name)
certificate = self._certificate_get(
cr, uid, ids, report, context=context)
if certificate and certificate.attachment:
signed_content = self._attach_signed_read(
cr, uid, ids, certificate, context=context)
if signed_content:
_logger.info("The signed PDF document '%s/%s' was loaded from "
"the database", report_name, ids)
return signed_content
content = super(Report, self).get_pdf(
cr, uid, ids, report_name, html=html, data=data,
context=context)
if certificate:
# Creating temporary origin PDF
pdf_fd, pdf = tempfile.mkstemp(
suffix='.pdf', prefix='report.tmp.')
with closing(os.fdopen(pdf_fd, 'w')) as pf:
pf.write(content)
_logger.info(
"Signing PDF document '%s/%s' with certificate '%s'",
report_name, ids, certificate.name)
signed = self.pdf_sign(pdf, certificate)
# Read signed PDF
if os.path.exists(signed):
with open(signed, 'rb') as pf:
content = pf.read()
# Manual cleanup of the temporary files
for fname in (pdf, signed):
try:
os.unlink(fname)
except (OSError, IOError):
_logger.error('Error when trying to remove file %s', fname)
if certificate.attachment:
self._attach_signed_write(
cr, uid, ids, certificate, content, context=context)
return content

42
report_qweb_signer/models/report_certificate.py

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
class ReportCertificate(models.Model):
_name = 'report.certificate'
_order = 'sequence,id'
@api.model
def _default_company(self):
m_company = self.env['res.company']
return m_company._company_default_get('report.certificate')
sequence = fields.Integer(default=10)
name = fields.Char(required=True)
path = fields.Char(
string="Certificate file path", required=True,
help="Path to PKCS#12 certificate file")
password_file = fields.Char(
string="Password file path", required=True,
help="Path to certificate password file")
model_id = fields.Many2one(
string="Model", required=True,
comodel_name='ir.model',
help="Model where apply this certificate")
domain = fields.Char(
string="Domain",
help="Domain for filtering if sign or not the document")
allow_only_one = fields.Boolean(
string="Allow only one document", default=True,
help="If True, this certificate can not be useb to sign "
"a PDF from several documents.")
attachment = fields.Char(
string="Save as attachment",
help="Filename used to store signed document as attachment. "
"Keep empty to not save signed document.")
company_id = fields.Many2one(
string='Company', comodel_name='res.company',
required=True, default=_default_company)

14
report_qweb_signer/models/res_company.py

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, fields
class ResCompany(models.Model):
_inherit = 'res.company'
report_certificate_ids = fields.One2many(
string="PDF report certificates",
comodel_name='report.certificate',
inverse_name='company_id')

3
report_qweb_signer/security/ir.model.access.csv

@ -0,0 +1,3 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_report_certificate_public","report_certificate group_public","model_report_certificate","base.group_user",1,0,0,0
"access_report_certificate_manager","report_certificate group_manager","model_report_certificate","base.group_erp_manager",1,1,1,1

BIN
report_qweb_signer/static/certificate/test.p12

1
report_qweb_signer/static/certificate/test.passwd

@ -0,0 +1 @@
admin

BIN
report_qweb_signer/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 2.8 KiB

1
report_qweb_signer/static/description/noun_65694_cc.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 125" enable-background="new 0 0 100 100" xml:space="preserve"><rect x="24.469" y="28.037" fill="#000000" width="51.159" height="5.592"/><rect x="24.469" y="16.401" fill="#000000" width="51.159" height="5.592"/><rect x="24.469" y="39.672" fill="#000000" width="51.159" height="5.592"/><rect x="24.469" y="51.309" fill="#000000" width="24.345" height="5.592"/><path fill="#000000" d="M80.018,5.03H19.982c-3.724,0-6.745,3.019-6.745,6.745v73.077c0,3.727,3.021,6.744,6.745,6.744h33.784v-4.66 H22.829c-2.485,0-4.498-2.014-4.498-4.496V14.187c0-2.483,2.013-4.496,4.498-4.496h54.342c2.483,0,4.498,2.013,4.498,4.496v68.252 c0,2.482-2.015,4.496-4.498,4.496h-1.819v4.66h4.666c3.726,0,6.746-3.019,6.746-6.744V11.775C86.764,8.049,83.742,5.03,80.018,5.03z "/><path fill="#000000" d="M75.308,69.185l5.763-2.743l-5.723-2.822l3.619-5.255l-6.369,0.417l0.51-6.362l-5.309,3.543l-2.741-5.763 l-2.823,5.722l-5.256-3.616l0.416,6.366l-6.36-0.506l3.544,5.306l-5.764,2.742l5.723,2.822l-3.619,5.256l6.367-0.415l-0.508,6.359 l5.309-3.545l2.742,5.765l2.822-5.723l5.255,3.618l-0.414-6.366l6.36,0.509L75.308,69.185z M73.956,63.635L64.892,74.36 c-0.456,0.546-1.134,0.862-1.847,0.867c-0.004,0-0.01,0-0.016,0c-0.707,0-1.379-0.308-1.843-0.838l-4.05-4.647 c-0.886-1.019-0.78-2.561,0.235-3.446c1.019-0.887,2.562-0.781,3.448,0.237l2.178,2.5l7.228-8.55 c0.868-1.03,2.411-1.162,3.441-0.289C74.696,61.065,74.826,62.605,73.956,63.635z"/><polygon fill="#000000" points="72.905,94.957 64.818,88.149 56.777,94.957 56.777,81.658 61.884,79.125 64.818,84.539 67.877,79.141 72.905,81.658 "/><text x="0" y="115" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Anton Noskov</text><text x="0" y="120" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>

BIN
report_qweb_signer/static/jar/itext-1.4.8.jar

BIN
report_qweb_signer/static/jar/jPdfSign.jar

333
report_qweb_signer/static/src/java/JPdfSign.java

@ -0,0 +1,333 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.ProviderException;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import sun.security.pkcs11.SunPKCS11;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfSignatureAppearance;
import com.lowagie.text.pdf.PdfStamper;
/*
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
*/
/**
*
* @author Jan Peter Stotz
*
*/
public class JPdfSign {
private static PrivateKey privateKey;
private static Certificate[] certificateChain;
private static ResourceBundle bundle = ResourceBundle.getBundle("strings");
private static String PRODUCTNAME = bundle.getString("productname");
private static String VERSION = bundle.getString("version");
private static String JAR_FILENAME = bundle.getString("jar-filename");
public static void main(String[] args) {
// for (int i = 0; i < args.length; i++) {
// System.out.println("arg[" + i + "] :" + args[i]);
// }
if (args.length < 2)
showUsage();
try {
String pkcs12FileName = args[0].trim();
String pdfInputFileName = args[1];
String pdfOutputFileName = args[2];
boolean usePKCS12 = !(pkcs12FileName.equals("-PKCS11"));
String passwdfile = "";
if (args.length == 4) {
passwdfile = args[3];
}
// System.out.println("");
// System.out.println("pdfInputFileName : " + pdfInputFileName);
// System.out.println("pdfOutputFileName: " + pdfOutputFileName);
if (usePKCS12)
readPrivateKeyFromPKCS12(pkcs12FileName, passwdfile);
else
readPrivateKeyFromPKCS11();
PdfReader reader = null;
try {
reader = new PdfReader(pdfInputFileName);
} catch (IOException e) {
System.err
.println("An unknown error accoured while opening the input PDF file: \""
+ pdfInputFileName + "\"");
e.printStackTrace();
System.exit(-1);
}
FileOutputStream fout = null;
try {
fout = new FileOutputStream(pdfOutputFileName);
} catch (FileNotFoundException e) {
System.err
.println("An unknown error accoured while opening the output PDF file: \""
+ pdfOutputFileName + "\"");
e.printStackTrace();
System.exit(-1);
}
PdfStamper stp = null;
try {
stp = PdfStamper.createSignature(reader, fout, '\0', null, true);
PdfSignatureAppearance sap = stp.getSignatureAppearance();
sap.setCrypto(privateKey, certificateChain, null, PdfSignatureAppearance.WINCER_SIGNED);
// sap.setCrypto(privateKey, certificateChain, null, null);
// sap.setReason("I'm the author");
// sap.setLocation("Lisbon");
// sap.setVisibleSignature(new Rectangle(100, 100, 200, 200), 1,
// null);
sap.setCertified(true);
stp.close();
} catch (Exception e) {
System.err
.println("An unknown error accoured while signing the PDF file:");
e.printStackTrace();
System.exit(-1);
}
} catch (KeyStoreException kse) {
System.err
.println("An unknown error accoured while initializing the KeyStore instance:");
kse.printStackTrace();
System.exit(-1);
}
}
private static void readPrivateKeyFromPKCS11() throws KeyStoreException {
// Initialize PKCS#11 provider from config file
String configFileName = getConfigFilePath("pkcs11.cfg");
Provider p = null;
try {
p = new SunPKCS11(configFileName);
Security.addProvider(p);
} catch (ProviderException e) {
System.err
.println("Unable to load PKCS#11 provider with config file: "
+ configFileName);
e.printStackTrace();
System.exit(-1);
}
String pkcs11PIN = "000000";
System.out.print("Please enter the smartcard pin: ");
try {
BufferedReader in = new BufferedReader(new InputStreamReader(
System.in));
pkcs11PIN = in.readLine();
// System.out.println(pkcs11PIN);
// System.out.println(pkcs11PIN.length());
} catch (Exception e) {
System.err
.println("An unknown error accoured while reading the PIN:");
e.printStackTrace();
System.exit(-1);
}
KeyStore ks = null;
try {
ks = KeyStore.getInstance("pkcs11", p);
ks.load(null, pkcs11PIN.toCharArray());
} catch (NoSuchAlgorithmException e) {
System.err
.println("An unknown error accoured while reading the PKCS#11 smartcard:");
e.printStackTrace();
System.exit(-1);
} catch (CertificateException e) {
System.err
.println("An unknown error accoured while reading the PKCS#11 smartcard:");
e.printStackTrace();
System.exit(-1);
} catch (IOException e) {
System.err
.println("An unknown error accoured while reading the PKCS#11 smartcard:");
e.printStackTrace();
System.exit(-1);
}
String alias = "";
try {
alias = (String) ks.aliases().nextElement();
privateKey = (PrivateKey) ks.getKey(alias, pkcs11PIN.toCharArray());
} catch (NoSuchElementException e) {
System.err
.println("An unknown error accoured while retrieving the private key:");
System.err
.println("The selected PKCS#12 file does not contain any private keys.");
e.printStackTrace();
System.exit(-1);
} catch (NoSuchAlgorithmException e) {
System.err
.println("An unknown error accoured while retrieving the private key:");
e.printStackTrace();
System.exit(-1);
} catch (UnrecoverableKeyException e) {
System.err
.println("An unknown error accoured while retrieving the private key:");
e.printStackTrace();
System.exit(-1);
}
certificateChain = ks.getCertificateChain(alias);
}
protected static void readPrivateKeyFromPKCS12(String pkcs12FileName, String pwdFile)
throws KeyStoreException {
String pkcs12Password = "";
KeyStore ks = null;
if (!pwdFile.equals("")) {
try {
FileInputStream pwdfis = new FileInputStream(pwdFile);
byte[] pwd = new byte[1024];
try {
do {
int r = pwdfis.read(pwd);
if (r < 0) {
break;
}
pkcs12Password += new String(pwd);
pkcs12Password = pkcs12Password.trim();
} while (pwdfis.available() > 0);
pwdfis.close();
} catch (IOException ex) {
System.err
.println("Can't read password file: " + pwdFile);
}
} catch (FileNotFoundException fnfex) {
System.err
.println("Password file not found: " + pwdFile);
}
} else {
System.out.print("Please enter the password for \"" + pkcs12FileName
+ "\": ");
try {
BufferedReader in = new BufferedReader(new InputStreamReader(
System.in));
pkcs12Password = in.readLine();
} catch (Exception e) {
System.err
.println("An unknown error accoured while reading the password:");
e.printStackTrace();
System.exit(-1);
}
}
try {
ks = KeyStore.getInstance("pkcs12");
ks.load(new FileInputStream(pkcs12FileName), pkcs12Password
.toCharArray());
} catch (NoSuchAlgorithmException e) {
System.err
.println("An unknown error accoured while reading the PKCS#12 file:");
e.printStackTrace();
System.exit(-1);
} catch (CertificateException e) {
System.err
.println("An unknown error accoured while reading the PKCS#12 file:");
e.printStackTrace();
System.exit(-1);
} catch (FileNotFoundException e) {
System.err.println("Unable to open the PKCS#12 keystore file \""
+ pkcs12FileName + "\":");
System.err
.println("The file does not exists or missing read permission.");
System.exit(-1);
} catch (IOException e) {
System.err
.println("An unknown error accoured while reading the PKCS#12 file:");
e.printStackTrace();
System.exit(-1);
}
String alias = "";
try {
alias = (String) ks.aliases().nextElement();
privateKey = (PrivateKey) ks.getKey(alias, pkcs12Password
.toCharArray());
} catch (NoSuchElementException e) {
System.err
.println("An unknown error accoured while retrieving the private key:");
System.err
.println("The selected PKCS#12 file does not contain any private keys.");
e.printStackTrace();
System.exit(-1);
} catch (NoSuchAlgorithmException e) {
System.err
.println("An unknown error accoured while retrieving the private key:");
e.printStackTrace();
System.exit(-1);
} catch (UnrecoverableKeyException e) {
System.err
.println("An unknown error accoured while retrieving the private key:");
e.printStackTrace();
System.exit(-1);
}
certificateChain = ks.getCertificateChain(alias);
}
protected static String getConfigFilePath(String configFilename) {
CodeSource source = JPdfSign.class.getProtectionDomain()
.getCodeSource();
URL url = source.getLocation();
String jarPath = URLDecoder.decode(url.getFile());
File f = new File(jarPath);
try {
jarPath = f.getCanonicalPath();
} catch (IOException e) {
}
if (!f.isDirectory()) {
f = new File(jarPath);
jarPath = f.getParent();
}
System.out.println(jarPath);
if (jarPath.length() > 0) {
return jarPath + File.separator + configFilename;
} else
return configFilename;
}
public static void showUsage() {
System.out.println("jPdfSign v" + VERSION
+ " by Jan Peter Stotz - jpstotz@gmx.de\n");
System.out.println(PRODUCTNAME + " usage:");
System.out
.println("\nFor using a PKCS#12 (.p12) file as signature certificate and private key source:");
System.out.print("\tjava -jar " + JAR_FILENAME);
System.out
.println(" pkcs12FileName pdfInputFileName pdfOutputFileName");
System.out
.println("\nFor using a PKCS#11 smartcard as signature certificate and private key source:");
System.out.print("\tjava -jar" + JAR_FILENAME);
System.out.println(" -PKCS11 pdfInputFileName pdfOutputFileName");
System.exit(0);
}
}

3
report_qweb_signer/static/src/java/strings.properties

@ -0,0 +1,3 @@
productname=jPdfSign
version=0.3.1
jar-filename=jPdfSign.jar

66
report_qweb_signer/views/report_certificate_view.xml

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015 Antiun Ingenieria S.L. - Antonio Espinosa
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<data>
<record id="view_report_certificate_form" model="ir.ui.view">
<field name="name">report.certificate.form</field>
<field name="model">report.certificate</field>
<field name="arch" type="xml">
<form string="PDF report certificate">
<sheet string="PDF report certificate">
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="path"/>
<field name="password_file"/>
<field name="model_id"/>
</group>
<group>
<field name="domain"/>
<field name="allow_only_one"/>
<field name="attachment"/>
<field name="company_id" widget="selection"
groups="base.group_multi_company"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_report_certificate_tree" model="ir.ui.view" >
<field name="name">report.certificate.tree</field>
<field name="model">report.certificate</field>
<field name="arch" type="xml">
<tree string="PDF report certificates">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="path"/>
<field name="model_id"/>
<field name="domain"/>
<field name="company_id"/>
</tree>
</field>
</record>
<record id="action_report_certificate" model="ir.actions.act_window">
<field name="name">PDF certificates</field>
<field name="res_model">report.certificate</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_report_certificate"
name="PDF certificates"
parent="report.reporting_menuitem"
action="action_report_certificate"/>
</data>
</openerp>

26
report_qweb_signer/views/res_company_view.xml

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015 Antiun Ingenieria S.L. - Antonio Espinosa
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<openerp>
<data>
<record id="view_company_form" model="ir.ui.view">
<field name="name">Add PDF report certificates list</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="model">res.company</field>
<field name="arch" type="xml">
<data>
<xpath expr="//page[@string='Report Configuration']/group[@string='Configuration']" position="after">
<group string="Certificates" col="2">
<field name="report_certificate_ids"
context="{'default_company_id': active_id}"/>
</group>
</xpath>
</data>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save