Antonio Espinosa
9 years ago
committed by
Pedro M. Baeza
21 changed files with 1069 additions and 0 deletions
-
116report_qweb_signer/README.rst
-
5report_qweb_signer/__init__.py
-
31report_qweb_signer/__openerp__.py
-
21report_qweb_signer/demo/report_certificate.xml
-
46report_qweb_signer/demo/report_partner.xml
-
161report_qweb_signer/i18n/es.po
-
7report_qweb_signer/models/__init__.py
-
193report_qweb_signer/models/report.py
-
42report_qweb_signer/models/report_certificate.py
-
14report_qweb_signer/models/res_company.py
-
3report_qweb_signer/security/ir.model.access.csv
-
BINreport_qweb_signer/static/certificate/test.p12
-
1report_qweb_signer/static/certificate/test.passwd
-
BINreport_qweb_signer/static/description/icon.png
-
1report_qweb_signer/static/description/noun_65694_cc.svg
-
BINreport_qweb_signer/static/jar/itext-1.4.8.jar
-
BINreport_qweb_signer/static/jar/jPdfSign.jar
-
333report_qweb_signer/static/src/java/JPdfSign.java
-
3report_qweb_signer/static/src/java/strings.properties
-
66report_qweb_signer/views/report_certificate_view.xml
-
26report_qweb_signer/views/res_company_view.xml
@ -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. |
@ -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 |
@ -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", |
|||
], |
|||
} |
@ -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> |
@ -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> |
@ -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)" |
|||
|
@ -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 |
@ -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 |
@ -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) |
@ -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') |
@ -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 |
@ -0,0 +1 @@ |
|||
admin |
After Width: 128 | Height: 128 | Size: 2.8 KiB |
@ -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> |
@ -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); |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
productname=jPdfSign |
|||
version=0.3.1 |
|||
jar-filename=jPdfSign.jar |
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue