[ADD] report_qweb_signer addon
parent
25f2a553ac
commit
fe8ffc011a
|
@ -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,191 @@
|
|||
# -*- 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):') + '\n',
|
||||
_('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__)
|
||||
return 'java -jar {}/../static/jar/jPdfSign.jar '.format(me) + 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') % (str(process.returncode), err))
|
||||
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
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
admin
|
Binary file not shown.
After Width: | Height: | 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>
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
Binary file not shown.
|
@ -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>
|
Loading…
Reference in New Issue