summaryrefslogtreecommitdiff
path: root/jasper_reports/JasperReports
diff options
context:
space:
mode:
Diffstat (limited to 'jasper_reports/JasperReports')
-rwxr-xr-xjasper_reports/JasperReports/__init__.py39
-rwxr-xr-xjasper_reports/JasperReports/abstract_data_generator.py40
-rwxr-xr-xjasper_reports/JasperReports/browse_data_generator.py492
-rwxr-xr-xjasper_reports/JasperReports/http_server.py102
-rwxr-xr-xjasper_reports/JasperReports/jasper_report.py352
-rwxr-xr-xjasper_reports/JasperReports/jasper_report_config.py260
-rwxr-xr-xjasper_reports/JasperReports/jasper_server.py126
-rwxr-xr-xjasper_reports/JasperReports/record_data_generator.py125
-rwxr-xr-xjasper_reports/JasperReports/websrv_lib.py245
9 files changed, 1781 insertions, 0 deletions
diff --git a/jasper_reports/JasperReports/__init__.py b/jasper_reports/JasperReports/__init__.py
new file mode 100755
index 0000000..8370f84
--- /dev/null
+++ b/jasper_reports/JasperReports/__init__.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+from . import abstract_data_generator
+from . import browse_data_generator
+from . import record_data_generator
+from . import jasper_report
+from . import jasper_server
+from . import http_server
+from . import jasper_report_config
+from . import websrv_lib
diff --git a/jasper_reports/JasperReports/abstract_data_generator.py b/jasper_reports/JasperReports/abstract_data_generator.py
new file mode 100755
index 0000000..c185887
--- /dev/null
+++ b/jasper_reports/JasperReports/abstract_data_generator.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+
+class AbstractDataGenerator:
+
+ def __init__(self):
+ pass
+
+ # Simple function all DataGenerators should implement
+ def generate(self, filename):
+ pass
diff --git a/jasper_reports/JasperReports/browse_data_generator.py b/jasper_reports/JasperReports/browse_data_generator.py
new file mode 100755
index 0000000..73fe1cf
--- /dev/null
+++ b/jasper_reports/JasperReports/browse_data_generator.py
@@ -0,0 +1,492 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (c) 2012 Omar Castiñeira Saavedra <omar@pexego.es>
+# Pexego Sistemas Informáticos http://www.pexego.es
+# Copyright (C) 2013 Tadeus Prastowo <tadeus.prastowo@infi-nity.com>
+# Vikasa Infinity Anugrah <http://www.infi-nity.com>
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+import os
+import csv
+import base64
+import tempfile
+import codecs
+import logging
+from xml.dom.minidom import getDOMImplementation
+
+from .abstract_data_generator import AbstractDataGenerator
+
+
+class BrowseDataGenerator(AbstractDataGenerator):
+ def __init__(self, report, model, env, cr, uid, ids, context):
+ self.report = report
+ self.model = model
+ self.env = env
+ self.cr = cr
+ self.uid = uid
+ self.ids = ids
+ self._context = context
+ self._languages = []
+ self.image_files = {}
+ self.temporary_files = []
+ self.logger = logging.getLogger(__name__)
+
+ def warning(self, message):
+ if self.logger:
+ self.logger.warning("%s" % message)
+
+ def languages(self):
+ if self._languages:
+ return self._languages
+ languages = self.env['res.lang'].search([('translatable', '=', '1')])
+ self._languages = languages.mapped('code')
+ return self._languages
+
+ def value_in_all_languages(self, model, id, field):
+ context = self.env.context.copy()
+ model = self.env[model]
+ values = {}
+
+ for language in self.languages():
+ if language == 'en_US':
+ context.update({'lang': False})
+ else:
+ context.update({'lang': language})
+ values[language] = model.browse(id).mapped(field)
+ if model._fields[field].type == 'selection' and \
+ model._fields[field].selection:
+ field_data = model.with_context(context).\
+ fields_get(allfields=[field])
+ values[language] = dict(field_data[field]['selection']).get(
+ values[language][0], values[language][0])
+ result = []
+ for key, value in values.items():
+ result.append('%s~%s' % (key, value))
+ return '|'.join(result)
+
+ def find_value_type(self, root, value_metadata):
+ if root in value_metadata.keys():
+ value_metadata2 = value_metadata[root]
+ if 'type' in value_metadata2.keys():
+ return value_metadata2['type']
+ return None
+
+ def generate_ids(self, record, relations, path, current_records):
+ unrepeated = set([field.partition('/')[0] for field in relations])
+ for relation in unrepeated:
+ value_type = None
+ root = relation.partition('/')[0]
+ if path:
+ current_path = '%s/%s' % (path, root)
+ else:
+ current_path = root
+
+ if root == 'Attachments':
+ value = self.env['ir.attachment'].search([
+ ('res_model', '=', record._name),
+ ('res_id', '=', record.id)])
+
+ elif root == 'User':
+ value = self.env['res.users'].browse([self.uid])
+ else:
+ if root == 'id':
+ value = record.id
+ value_metadata = record.fields_get([root])
+ value_type = self.find_value_type(root, value_metadata)
+ elif hasattr(record, root):
+ value = getattr(record, root)
+ value_metadata = record.fields_get([root])
+ value_type = self.find_value_type(root, value_metadata)
+ else:
+ warning = "Field '%s' does not exist in model '%s'."
+ self.warning(warning % (root, record._name))
+ continue
+
+ if value_type == 'many2one':
+ relations2 = [
+ field.partition('/')[2] for field in relations
+ if field.partition('/')[0] == root and
+ field.partition('/')[2]]
+ return self.generate_ids(
+ value, relations2, current_path, current_records)
+
+ if not (value_type == 'one2many' or value_type == 'many2many'):
+ wrng2 = "Field '%s' in model '%s' is not a relation field."
+ self.warning(wrng2 % (root, self.model))
+ return current_records
+
+ # Only join if there are any records because it's a LEFT JOIN
+ # If we wanted an INNER JOIN we wouldn't check for "value" and
+ # return an empty current_records
+ if value:
+ # Only
+ new_records = []
+ for v in value:
+ current_new_records = []
+
+ for rec_id in current_records:
+ new = rec_id.copy()
+ new[current_path] = v
+ current_new_records.append(new)
+
+ relations2 = [
+ field.partition('/')[2] for field in relations
+ if field.partition('/')[0] == root and
+ field.partition('/')[2]]
+ new_records += self.generate_ids(
+ v, relations2, current_path, current_new_records)
+
+ current_records = new_records
+ return current_records
+
+
+class XmlBrowseDataGenerator(BrowseDataGenerator):
+ def __init__(self, report, model, env, cr, uid, ids, context):
+ super(XmlBrowseDataGenerator, self).__init__(
+ report, model, env, cr, uid, ids, context)
+ self.all_records = []
+ self.document = None
+
+ # XML file generation works as follows:
+ # By default (if no ODOO_RELATIONS property exists in the report)
+ # a record will be created for each model id we've been asked to show.
+ # If there are any elements in the ODOO_RELATIONS list,
+ # they will imply a LEFT JOIN like behavior on the rows to be shown.
+ def generate(self, file_name):
+ self.all_records = []
+ relations = self.report.relations
+ # The following loop generates one entry to all_records list
+ # for each record that will be created. If there are any relations
+ # it acts like a LEFT JOIN against the main model/table.
+ for record in self.env[self.model].browse(self.ids):
+
+ new_records = self.generate_ids(
+ record, relations, '', [{'root': record}])
+ copies = 1
+ if self.report.copies_field and \
+ record.__hasattr__(self.report.copies_field):
+ copies = int(record.__getattr__(self.report.copies_field))
+ for new in new_records:
+ for x in range(copies):
+ self.all_records.append(new)
+
+ # Once all records have been calculated, create the
+ # XML structure itself
+ self.document = getDOMImplementation().createDocument(
+ None, 'data', None)
+ top_node = self.document.documentElement
+ for records in self.all_records:
+ record_node = self.document.createElement('record')
+ top_node.appendChild(record_node)
+ self.generate_xml_record(
+ records['root'], records, record_node, '', self.report.fields)
+
+ # Once created, the only missing step is to store the XML into a file
+ with codecs.open(file_name, 'wb+', 'utf-8') as f:
+ top_node.writexml(f)
+
+ def generate_xml_record(self, record, records, record_node, path, fields):
+ # One field (many2one, many2many or one2many) can appear several times.
+ # Process each "root" field only once by using a set.
+ unrepeated = set([field.partition('/')[0] for field in fields])
+
+ for field in unrepeated:
+ root = field.partition('/')[0]
+ if path:
+ current_path = '%s/%s' % (path, root)
+ else:
+ current_path = root
+ field_node = self.document.createElement(root)
+ record_node.appendChild(field_node)
+
+ if root == 'Attachments':
+ value = self.env['ir.attachment'].search(
+ [('res_model', '=', record._name),
+ ('res_id', '=', record.id)])
+
+ elif root == 'User':
+ value = self.env.user
+ else:
+ if root == 'id':
+ value = record.id
+ value_metadata = record.fields_get([root])
+ value_type = self.find_value_type(root, value_metadata)
+ elif hasattr(record, root):
+ value = getattr(record, root)
+ value_metadata = record.fields_get([root])
+ value_type = self.find_value_type(root, value_metadata)
+ else:
+ value = None
+ wrng4 = "Field '%s' does not exist in model '%s'."
+ self.warning(wrng4 % (root, record._name))
+
+ # Check if it's a many2one
+ if value_type == 'many2one':
+ fields2 = [f.partition('/')[2] for f in fields
+ if f.partition('/')[0] == root]
+ self.generate_xml_record(
+ value, records, field_node, current_path, fields2)
+ continue
+
+ # Check if it's a one2many or many2many
+ if value_type == 'one2many' or value_type == 'many2many':
+ if not value:
+ continue
+
+ fields2 = [f.partition('/')[2] for f in fields
+ if f.partition('/')[0] == root]
+ if current_path in records:
+ self.generate_xml_record(
+ records[current_path], records,
+ field_node, current_path, fields2)
+ else:
+ # If the field is not marked to be iterated use
+ # the first record only
+ self.generate_xml_record(
+ value[0], records, field_node, current_path, fields2)
+ continue
+
+ if field in record._fields:
+ field_type = record._fields[field].type
+
+ # The rest of field types must be converted into str
+ if field == 'id':
+ # Check for field 'id' because we can't find it's
+ # type in _columns
+ value = str(value)
+ elif value is False:
+ value = ''
+ elif field_type == 'date':
+ value = '%s 00:00:00' % str(value)
+ elif field_type == 'binary':
+ image_id = (record.id, field)
+ if image_id in self.image_files:
+ file_name = self.image_files[image_id]
+ else:
+ fd, file_name = tempfile.mkstemp()
+ try:
+ os.write(fd, base64.decodestring(value))
+ finally:
+ os.close(fd)
+ self.temporary_files.append(file_name)
+ self.image_files[image_id] = file_name
+ value = file_name
+
+ elif isinstance(value, str):
+ value = str(value, 'utf-8')
+ elif isinstance(value, float):
+ value = '%.10f' % value
+ elif not isinstance(value, str):
+ value = str(value)
+
+ value_node = self.document.createTextNode(value)
+ field_node.appendChild(value_node)
+
+
+class CsvBrowseDataGenerator(BrowseDataGenerator):
+ # CSV file generation works as follows:
+ # By default (if no ODOO_RELATIONS property exists in the report)
+ # a record will be created for each model id we've been asked to show.
+ # If there are any elements in the ODOO_RELATIONS list,
+ # they will imply a LEFT JOIN like behavior on the rows to be shown.
+ def generate(self, file_name):
+ self.all_records = []
+ relations = self.report.relations
+
+ # The following loop generates one entry to allRecords list
+ # for each record that will be created. If there are any relations
+ # it acts like a LEFT JOIN against the main model/table.
+ reportCopies = self.report.copies or 1
+ sequence = 0
+ copiesField = self.report.copies_field
+ for record in self.env[self.model].browse(self.ids):
+ newRecords = self.generate_ids(
+ record, relations, '', [{'root': record}])
+ copies = reportCopies
+ if copiesField and record.__hasattr__(copiesField):
+ copies = copies * int(record.__getattr__(copiesField))
+ sequence += 1
+ subsequence = 0
+ for new in newRecords:
+ new['sequence'] = sequence
+ new['subsequence'] = subsequence
+ subsequence += 1
+ for x in range(copies):
+ new['copy'] = x
+ self.all_records.append(new.copy())
+ with open(file_name, 'w') as csvfile:
+ fieldnames = self.report.field_names + ['']
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
+ # writer.writeheader() #header should only printed from jrxml file.
+ header = {}
+ for field in self.report.field_names + ['']:
+ header[field] = field
+ writer.writerow(header)
+
+ # Once all records have been calculated,
+ # create the CSV structure itself
+ for records in self.all_records:
+ row = {}
+ self.generateCsvRecord(
+ records['root'], records, row, '',
+ self.report.fields,
+ records['sequence'],
+ records['subsequence'],
+ records['copy'])
+
+ writer.writerow(row)
+
+ def generateCsvRecord(self, record, records, row, path, fields, sequence,
+ subsequence, copy):
+ # One field (many2one, many2many or one2many) can appear several times
+ # Process each "root" field only once by using a set.
+ unrepeated = set([field.partition('/')[0] for field in fields])
+
+ for field in unrepeated:
+ value_type = None
+ root = field.partition('/')[0]
+ current_path = root
+ if path:
+ current_path = '%s/%s' % (path, root)
+
+ if root == 'Attachments':
+ value = self.env['ir.attachment'].search(
+ [('res_model', '=', record._name),
+ ('res_id', '=', record.id)])
+
+ elif root == 'User':
+ value = self.env['res.users'].browse(self.uid)
+ elif root == 'Special':
+ fields2 = [f.partition('/')[2] for f in fields
+ if f.partition('/')[0] == root]
+
+ for f in fields2:
+ p = '%s/%s' % (current_path, f)
+ if f == 'sequence':
+ row[self.report.fields[p]['name']] = sequence
+ elif f == 'subsequence':
+ row[self.report.fields[p]['name']] = subsequence
+ elif f == 'copy':
+ row[self.report.fields[p]['name']] = copy
+ continue
+ else:
+ if root == 'id':
+ value = record.id
+ value_metadata = record.fields_get([root])
+ value_type = self.find_value_type(root, value_metadata)
+ elif hasattr(record, root):
+ value = getattr(record, root)
+ value_metadata = record.fields_get([root])
+ value_type = self.find_value_type(root, value_metadata)
+ else:
+ value = None
+ if root:
+ wrng6 = ("Field '%s' (path: %s) does"
+ "not exist in model '%s'.")
+ self.warning(
+ wrng6 % (root, current_path, record._name))
+
+ # Check if it's a many2one
+ if value_type == 'many2one':
+ fields2 = [f.partition('/')[2] for f in fields
+ if f.partition('/')[0] == root]
+ self.generateCsvRecord(
+ value, records, row, current_path,
+ fields2, sequence, subsequence, copy)
+ continue
+
+ # Check if it's a one2many or many2many
+ if value_type == 'one2many' or value_type == 'many2many':
+ if not value:
+ continue
+ fields2 = [f.partition('/')[2] for f in fields
+ if f.partition('/')[0] == root]
+ if current_path in records:
+ self.generateCsvRecord(
+ records[current_path], records, row,
+ current_path, fields2, sequence, subsequence, copy)
+ else:
+ # If the field is not marked to be iterated
+ # use the first record only
+ self.generateCsvRecord(
+ value[0], records, row,
+ current_path, fields2, sequence, subsequence, copy)
+ continue
+
+ # The field might not appear in the self.report.fields
+ # only when the field is a many2one but in this case it's null.
+ # This will make the path to look like: "journal_id",
+ # when the field actually in the report is "journal_id/name",
+ # for example.In order not to change the way we detect many2one
+ # fields, we simply check that the field is in self.report.
+ # fields() and that's it.
+ if current_path not in self.report.fields:
+ continue
+
+ # Show all translations for a field
+ type = self.report.fields[current_path]['type']
+ if type == 'java.lang.Object' and record.id:
+ value = self.value_in_all_languages(
+ record._name, record.id, root)
+
+ if field in record._fields:
+ field_type = record._fields[field].type
+
+ # The rest of field types must be converted into str
+ if field == 'id':
+ # Check for field 'id' because we can't find it's
+ # type in _columns
+ value = str(value)
+ elif value in (False, None):
+ value = ''
+ elif field_type == 'date':
+ value = '%s 00:00:00' % str(value)
+ elif field_type == 'binary':
+
+ image_id = (record.id, field)
+
+ if image_id in self.image_files:
+ file_name = self.image_files[image_id]
+ else:
+ fd, file_name = tempfile.mkstemp()
+ try:
+ os.write(fd, base64.decodestring(value))
+ finally:
+ os.close(fd)
+ self.temporary_files.append(file_name)
+ self.image_files[image_id] = file_name
+ value = file_name
+ elif isinstance(value, str):
+ value = value
+ elif isinstance(value, float):
+ value = '%.10f' % value
+ elif not isinstance(value, str):
+ value = str(value)
+ row[self.report.fields[current_path]['name']] = value
diff --git a/jasper_reports/JasperReports/http_server.py b/jasper_reports/JasperReports/http_server.py
new file mode 100755
index 0000000..0794d7d
--- /dev/null
+++ b/jasper_reports/JasperReports/http_server.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (c) 2012 Omar Castiñeira Saavedra <omar@pexego.es>
+# Pexego Sistemas Informáticos http://www.pexego.es
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+from http.server import BaseHTTPRequestHandler
+from odoo import netsvc
+from odoo import tools
+from .websrv_lib import reg_http_service
+
+
+class Message:
+ def __init__(self):
+ self.status = False
+
+
+class JasperHandler(BaseHTTPRequestHandler):
+ cache = {}
+
+ def __init__(self, request, client_address, server):
+ BaseHTTPRequestHandler.__init__(self, request, client_address, server)
+
+ def do_OPTIONS(self):
+ pass
+
+ def parse_request(self, *args, **kwargs):
+ path = self.raw_requestline.replace('GET', '').strip().split(' ')[0]
+ try:
+ result = self.execute(path)
+ except Exception as e:
+ result = '<error><exception>%s</exception></error>' % (e.args,)
+ self.wfile.write(result)
+ return True
+
+ def execute(self, path):
+ path = path.strip('/')
+ path = path.split('?')
+ model = path[0]
+ arguments = {}
+ for argument in path[-1].split('&'):
+ argument = argument.split('=')
+ arguments[argument[0]] = argument[-1]
+
+ use_cache = tools.config.get('jasper_cache', True)
+ database = arguments.get(
+ 'database', tools.config.get('jasper_database', 'stable8'))
+ user = arguments.get('user', tools.config.get('jasper_user', 'admin'))
+ password = arguments.get(
+ 'password', tools.config.get('jasper_password', 'a'))
+ depth = int(arguments.get('depth', tools.config.get(
+ 'jasper_depth', 3)))
+ language = arguments.get(
+ 'language', tools.config.get('jasper_language', 'en'))
+ # Check if data is in cache already
+ key = '%s|%s|%s|%s|%s' % (model, database, user, depth, language)
+ if key in self.cache:
+ return self.cache[key]
+
+ context = {'lang': language}
+ uid = netsvc.dispatch_rpc(
+ 'common', 'login', (database, user, password))
+ result = netsvc.dispatch_rpc(
+ 'object', 'execute',
+ (database, uid, password,
+ 'ir.actions.report',
+ 'create_xml', model, depth, context))
+
+ if use_cache:
+ self.cache[key] = result
+
+ return result
+
+
+reg_http_service('/jasper/', JasperHandler)
diff --git a/jasper_reports/JasperReports/jasper_report.py b/jasper_reports/JasperReports/jasper_report.py
new file mode 100755
index 0000000..1a1d9dc
--- /dev/null
+++ b/jasper_reports/JasperReports/jasper_report.py
@@ -0,0 +1,352 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (C) 2013 Tadeus Prastowo <tadeus.prastowo@infi-nity.com>
+# Vikasa Infinity Anugrah <http://www.infi-nity.com>
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+import os
+from lxml import etree
+import re
+
+try:
+ from tools.safe_eval import safe_eval
+ import tools
+except ImportError:
+ from odoo.tools.safe_eval import safe_eval
+ from odoo import tools
+
+DATA_SOURCE_EXPRESSION_REG_EXP = re.compile(r"""\$P\{(\w+)\}""")
+
+
+class JasperReport:
+ def __init__(self, file_name='', path_prefix=''):
+ self.report_path = file_name
+ self.path_prefix = path_prefix.strip()
+
+ if self.path_prefix and self.path_prefix[-1] != '/':
+ self.path_prefix += '/'
+
+ self.language = 'xpath'
+ self.relations = []
+ self.fields = {}
+ self.field_names = []
+ self.subreports = []
+ self.datasets = []
+ self.copies = 1
+ self.copies_field = False
+ self.is_header = False
+ if file_name:
+ self.extract_properties()
+
+ def subreport_directory(self):
+ return os.path.join(os.path.abspath(
+ os.path.dirname(self.report_path)), '')
+
+ def standard_directory(self):
+ jasperdir = tools.config.get('jasperdir')
+ if jasperdir:
+ if jasperdir.endswith(os.sep):
+ return jasperdir
+ else:
+ return os.path.join(jasperdir, '')
+ return os.path.join(
+ os.path.abspath(os.path.dirname(__file__)), '..', 'report', '')
+
+ def extract_fields(self, field_tags, ns):
+ # fields and fieldNames
+ fields = {}
+ field_names = []
+ for tag in field_tags:
+ name = tag.get('name')
+ type = tag.get('class')
+ path = tag.findtext('{%s}fieldDescription' % ns, '').strip()
+ # Make the path relative if it isn't already
+ if path.startswith('/data/record/'):
+ path = self.path_prefix + path[13:]
+
+ # Remove language specific data from the path so:
+ # Empresa-partner_id/Nom-name becomes partner_id/name
+ # We need to consider the fact that the name in user's language
+ # might not exist, hence the easiest thing to do is split and [-1]
+ new_path = [x.split('-')[-1] for x in path.split('/')]
+
+ path = '/'.join(new_path)
+ fields[path] = {
+ 'name': name,
+ 'type': type,
+ }
+ field_names.append(name)
+
+ return fields, field_names
+
+ def extract_properties(self):
+ # The function will read all relevant information from the jrxml file
+
+ doc = etree.parse(self.report_path)
+
+ # Define namespaces
+ ns = 'http://jasperreports.sourceforge.net/jasperreports'
+ nss = {'jr': ns}
+
+ # Language
+ # is XPath.
+ lang_tags = doc.xpath(
+ '/jr:jasperReport/jr:queryString', namespaces=nss)
+ if lang_tags:
+ if lang_tags[0].get('language'):
+ self.language = lang_tags[0].get('language').lower()
+
+ # Relations
+ ex_path = '/jr:jasperReport/jr:property[@name="ODOO_RELATIONS"]'
+ relation_tags = doc.xpath(ex_path, namespaces=nss)
+
+ if relation_tags[0].text is not None:
+ relation = relation_tags[0].text.strip()
+ # relation = '[%s]' % relation
+ self.relations = [x.strip() for x in relation.split(',')]
+ if relation.startswith('['):
+ self.relations = safe_eval(relation, {})
+ self.relations = [self.path_prefix + x for x in self.relations]
+
+ if not self.relations and self.path_prefix:
+ self.relations = [self.path_prefix[:-1]]
+
+ # Repeat field
+ path1 = '/jr:jasperReport/jr:property[@name="ODOO_COPIES_FIELD"]'
+ copies_field_tags = doc.xpath(path1, namespaces=nss)
+ if copies_field_tags and 'value' in copies_field_tags[0].keys():
+ self.copies_field = (
+ self.path_prefix + copies_field_tags[0].get('value'))
+
+ # Repeat
+ path2 = '/jr:jasperReport/jr:property[@name="ODOO_COPIES"]'
+ copies_tags = doc.xpath(path2, namespaces=nss)
+ if copies_tags and 'value' in copies_tags[0].keys():
+ self.copies = int(copies_tags[0].get('value'))
+
+ self.is_header = False
+ path3 = '/jr:jasperReport/jr:property[@name="ODOO_HEADER"]'
+ header_tags = doc.xpath(path3, namespaces=nss)
+ if header_tags and 'value' in header_tags[0].keys():
+ self.is_header = True
+
+ field_tags = doc.xpath('/jr:jasperReport/jr:field', namespaces=nss)
+ self.fields, self.field_names = self.extract_fields(field_tags, ns)
+
+ # Subreports
+ # Here we expect the following structure in the .jrxml file:
+ # <subreport>
+ # <dataSourceExpression><![CDATA[$P{REPORT_DATA_SOURCE}]]>
+ # </dataSourceExpression>
+ # <subreportExpression class="java.lang.String">
+ # <![CDATA[$P{STANDARD_DIR} + "report_header.jasper"]]>
+ # </subreportExpression>
+ # </subreport>
+
+ subreport_tags = doc.xpath('//jr:subreport', namespaces=nss)
+
+ for tag in subreport_tags:
+ text1 = '{%s}dataSourceExpression'
+ data_source_expression = tag.findtext(text1 % ns, '')
+
+ if not data_source_expression:
+ continue
+
+ data_source_expression = data_source_expression.strip()
+ m = DATA_SOURCE_EXPRESSION_REG_EXP.match(data_source_expression)
+
+ if not m:
+ continue
+
+ data_source_expression = m.group(1)
+ if data_source_expression == 'REPORT_DATA_SOURCE':
+ continue
+
+ subreport_expression = tag.findtext(
+ '{%s}subreportExpression' % ns, '')
+ if not subreport_expression:
+ continue
+ subreport_expression = subreport_expression.strip()
+ subreport_expression = (
+ subreport_expression.replace
+ ('$P{STANDARD_DIR}', '"%s"' % self.standard_directory()))
+ subreport_expression = (
+ subreport_expression.replace
+ ('$P{SUBREPORT_DIR}', '"%s"' % self.subreport_directory()))
+ try:
+ subreport_expression = safe_eval(subreport_expression, {})
+ except Exception:
+ continue
+ if subreport_expression.endswith('.jasper'):
+ subreport_expression = subreport_expression[:-6] + 'jrxml'
+
+ # Model
+ model = ''
+ path4 = '//jr:reportElement/jr:property[@name="ODOO_MODEL"]'
+ model_tags = tag.xpath(path4, namespaces=nss)
+ if model_tags and 'value' in model_tags[0].keys():
+ model = model_tags[0].get('value')
+
+ path_prefix = ''
+ pat = '//jr:reportElement/jr:property[@name="ODOO_PATH_PREFIX"]'
+ path_prefix_tags = tag.xpath(pat, namespaces=nss)
+ if path_prefix_tags and 'value' in path_prefix_tags[0].keys():
+ path_prefix = path_prefix_tags[0].get('value')
+
+ self.is_header = False
+ path5 = '//jr:reportElement/jr:property[@name="ODOO_HEADER"]'
+ header_tags = tag.xpath(path5, namespaces=nss)
+
+ if header_tags and 'value' in header_tags[0].keys():
+ self.is_header = True
+
+ # Add our own path_prefix to subreport's path_prefix
+ sub_prefix = []
+
+ if self.path_prefix:
+ sub_prefix.append(self.path_prefix)
+ if path_prefix:
+ sub_prefix.append(path_prefix)
+
+ sub_prefix = '/'.join(sub_prefix)
+
+ subreport = JasperReport(subreport_expression, sub_prefix)
+
+ self.subreports.append({
+ 'parameter': data_source_expression,
+ 'filename': subreport_expression,
+ 'model': model,
+ 'pathPrefix': path_prefix,
+ 'report': subreport,
+ 'depth': 1})
+ for subsub_info in subreport.subreports:
+ subsub_info['depth'] += 1
+ # Note hat 'parameter' (the one used to pass report's
+ # DataSource) must be the same in all reports
+ self.subreports.append(subsub_info)
+
+ # Dataset
+ # Here we expect the following structure in the .jrxml file:
+ # <datasetRun>
+ # <dataSourceExpression><![CDATA[$P{REPORT_DATA_SOURCE}]]>
+ # </dataSourceExpression>
+ # </datasetRun>
+
+ dataset_tags = doc.xpath('//jr:datasetRun', namespaces=nss)
+
+ for tag in dataset_tags:
+ path7 = '{%s}dataSourceExpression'
+ data_source_expression = tag.findtext(path7 % ns, '')
+ if not data_source_expression:
+ continue
+ data_source_expression = data_source_expression.strip()
+ m = DATA_SOURCE_EXPRESSION_REG_EXP.match(data_source_expression)
+ if not m:
+ continue
+ data_source_expression = m.group(1)
+ if data_source_expression == 'REPORT_DATA_SOURCE':
+ continue
+ sub_dataset_name = tag.get('subDataset')
+ if not sub_dataset_name:
+ continue
+
+ # Relations
+ relations = []
+ path8 = \
+ '../../jr:reportElement/jr:property[@name="ODOO_RELATIONS"]'
+ relation_tags = tag.xpath(path8, namespaces=nss)
+
+ if relation_tags and 'value' in relation_tags[0].keys():
+ relation = relation_tags[0].get('value').strip()
+
+ if relation.startswith('['):
+ relations = safe_eval(relation_tags[0].get('value'), {})
+ else:
+ relations = [x.strip() for x in relation.split(',')]
+
+ relations = [self.path_prefix + x for x in relations]
+
+ if not relations and self.path_prefix:
+ relations = [self.path_prefix[:-1]]
+
+ # Repeat field
+ copies_field = None
+ path9 = ('../../jr:reportElement/jr:property'
+ '[@name="ODOO_COPIES_FIELD"]')
+ copies_field_tags = tag.xpath(path9, namespaces=nss)
+ if copies_field_tags and 'value' in copies_field_tags[0].keys():
+ copies_field = \
+ self.path_prefix + copies_field_tags[0].get('value')
+
+ # Repeat
+ copies = None
+ path11 = \
+ '../../jr:reportElement/jr:property[@name="ODOO_COPIES"]'
+ copies_tags = tag.xpath(path11, namespaces=nss)
+ if copies_tags and 'value' in copies_tags[0].keys():
+ copies = int(copies_tags[0].get('value'))
+
+ # Model
+ model = ''
+ path12 = \
+ '../../jr:reportElement/jr:property[@name="ODOO_MODEL"]'
+ model_tags = tag.xpath(path12, namespaces=nss)
+ if model_tags and 'value' in model_tags[0].keys():
+ model = model_tags[0].get('value')
+
+ path_prefix = ''
+ path13 = ('../../jr:reportElement/jr:property'
+ '[@name="ODOO_PATH_PREFIX"]')
+ path_prefix_tags = tag.xpath(path13, namespaces=nss)
+
+ if path_prefix_tags and 'value' in path_prefix_tags[0].keys():
+ path_prefix = path_prefix_tags[0].get('value')
+
+ # We need to find the appropriate subDataset definition
+ # for this dataset run.
+ path14 = '//jr:subDataset[@name="%s"]'
+ sub_dataset = doc.xpath(
+ path14 % sub_dataset_name, namespaces=nss)[0]
+ field_tags = sub_dataset.xpath('jr:field', namespaces=nss)
+ fields, field_names = self.extract_fields(field_tags, ns)
+
+ dataset = JasperReport()
+ dataset.fields = fields
+ dataset.field_names = field_names
+ dataset.relations = relations
+ dataset.copies_field = copies_field
+ dataset.copies = copies
+ self.subreports.append({
+ 'parameter': data_source_expression,
+ 'model': model,
+ 'pathPrefix': path_prefix,
+ 'report': dataset,
+ 'filename': 'DATASET',
+ }) \ No newline at end of file
diff --git a/jasper_reports/JasperReports/jasper_report_config.py b/jasper_reports/JasperReports/jasper_report_config.py
new file mode 100755
index 0000000..47b11dd
--- /dev/null
+++ b/jasper_reports/JasperReports/jasper_report_config.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (C) 2013 Tadeus Prastowo <tadeus.prastowo@infi-nity.com>
+# Vikasa Infinity Anugrah <http://www.infi-nity.com>
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+import tempfile
+import logging
+import os
+import time
+import json
+
+from odoo import tools
+from .browse_data_generator import CsvBrowseDataGenerator
+from .jasper_server import JasperServer
+from .record_data_generator import CsvRecordDataGenerator
+from .jasper_report import JasperReport
+
+# Determines the port where the JasperServer process should listen
+# with its XML-RPC server for incoming calls
+
+tools.config['jasperport'] = tools.config.get('jasperport', 8090)
+
+# Determines the file name where the process ID of the
+# JasperServer process should be stored
+tools.config['jasperpid'] = tools.config.get('jasperpid', 'odoo-jasper.pid')
+
+# Determines if temporary files will be removed
+tools.config['jasperunlink'] = tools.config.get('jasperunlink', True)
+
+
+class Report:
+ def __init__(self, name, cr, uid, ids, data, context):
+ data.update({'report_type': 'jasper'})
+ self.name = name
+ self.env = data['env']
+ self.cr = cr
+ self.uid = uid
+ self.ids = ids
+ self.data = data
+ self.model = self.data.get('model', False) or context.get(
+ 'active_model', False)
+ self.context = context or {}
+ self.report_path = None
+ self.report = None
+ self.temporary_files = []
+ self.output_format = 'pdf'
+
+ def execute(self):
+ """
+ If self.context contains "return_pages = True" it will return
+ the number of pages of the generated report.
+ """
+ logger = logging.getLogger(__name__)
+ # * Get report path *
+ # Not only do we search the report by name but also ensure that
+ # 'report_rml' field has the '.jrxml' postfix. This is needed because
+ # adding reports using the <report/> tag, doesn't remove the old
+ # report record if the id already existed (ie. we're trying to
+ # override the 'purchase.order' report in a new module).
+ # As the previous record is not removed, we end up with two records
+ # named 'purchase.order' so we need to distinguish
+ # between the two by searching '.jrxml' in report_rml.
+ rep_xml_set = self.env['ir.actions.report'].search(
+ [('report_name', '=', self.name[7:]),
+ ('report_file', 'ilike', '.jrxml')])
+
+ for data in rep_xml_set:
+
+ if data.jasper_output:
+ self.output_format = data.jasper_output
+
+ self.report_path = self.addons_path(data.report_file)
+
+ if not os.path.lexists(self.report_path):
+ self.report_path = self.addons_path(path=data.report_file)
+
+ # Get report information from the jrxml file
+ logger.info("Requested report: '%s'" % self.report_path)
+ self.report = JasperReport(self.report_path)
+
+ # Create temporary input (XML) and output (PDF) files
+ fd, data_file = tempfile.mkstemp()
+ os.close(fd)
+ fd, output_file = tempfile.mkstemp()
+ os.close(fd)
+
+ self.temporary_files.append(data_file)
+ self.temporary_files.append(output_file)
+
+ logger.info("Temporary data file: '%s'" % data_file)
+ start = time.time()
+
+ # If the language used is xpath create the xmlFile in dataFile.
+ if self.report.language == 'xpath':
+ if self.data.get('data_source', 'model') == 'records':
+ generator = CsvRecordDataGenerator(
+ self.report, self.data['records'])
+ else:
+ generator = CsvBrowseDataGenerator(
+ self.report, self.model, self.env, self.cr,
+ self.uid, self.ids, self.context)
+ generator.generate(data_file)
+ self.temporary_files += generator.temporary_files
+
+ sub_report_data_files = []
+ for sub_report_info in self.report.subreports:
+ sub_report = sub_report_info['report']
+ if sub_report.language == 'xpath':
+ message = 'Creating CSV '
+
+ if sub_report_info['pathPrefix']:
+ message += 'with prefix %s ' % \
+ sub_report_info['pathPrefix']
+ else:
+ message += 'without prefix '
+
+ message += 'for file %s' % sub_report_info['filename']
+ logger.info("%s" % message)
+
+ fd, sub_report_data_file = tempfile.mkstemp()
+ os.close(fd)
+
+ sub_report_data_files.append({
+ 'parameter': sub_report_info['parameter'],
+ 'dataFile': sub_report_data_file,
+ 'jrxmlFile': sub_report_info['filename'],
+ })
+ self.temporary_files.append(sub_report_data_file)
+
+ if sub_report.is_header:
+ generator = CsvBrowseDataGenerator(
+ sub_report, 'res.users', self.env,
+ self.cr, self.uid, [self.uid], self.context)
+ elif self.data.get('data_source', 'model') == 'records':
+ generator = CsvRecordDataGenerator(
+ sub_report, self.data['records'])
+ else:
+ generator = CsvBrowseDataGenerator(
+ sub_report, self.model, self.env, self.cr,
+ self.uid, self.ids, self.context)
+ generator.generate(sub_report_data_file)
+
+ # Call the external java application that will generate the
+ # PDF file in outputFile
+ pages = self.execute_report(
+ data_file, output_file, sub_report_data_files)
+
+ elapsed = (time.time() - start) / 60
+ logger.info("ELAPSED: %f" % elapsed)
+
+ # Read data from the generated file and return it
+ with open(output_file, 'rb') as f:
+ data = f.read()
+
+ # Remove all temporary files created during the report
+ if tools.config['jasperunlink']:
+
+ for f in self.temporary_files:
+ try:
+ os.unlink(f)
+ except os.error:
+ logger.warning("Could not remove file '%s'." % f)
+
+ self.temporary_files = []
+
+ if self.context.get('return_pages'):
+ return data, self.output_format, pages
+ else:
+ return data
+
+ def path(self):
+ return os.path.abspath(os.path.dirname(__file__))
+
+ def addons_path(self, path=False):
+ if path:
+ report_module = path.split(os.path.sep)[0]
+
+ for addons_path in tools.config['addons_path'].split(','):
+ if os.path.lexists(addons_path + os.path.sep + report_module):
+ return os.path.normpath(addons_path + os.path.sep + path)
+
+ return os.path.dirname(self.path())
+
+ def system_user_name(self):
+ if os.name == 'nt':
+ import win32api
+ return win32api.GetUserName()
+ else:
+ import pwd
+ return pwd.getpwuid(os.getuid())[0]
+
+ def dsn(self):
+ host = tools.config['db_host'] or 'localhost'
+ port = tools.config['db_port'] or '5432'
+ db_name = self.cr.dbname
+ return 'jdbc:postgresql://%s:%s/%s' % (host, port, db_name)
+
+ def user_name(self):
+ # To allow all users to get db_user we have to call it with sudo
+ user_name = self.env['ir.config_parameter'].sudo().get_param(
+ 'db_user') or self.system_user_name()
+ return tools.config['db_user'] or user_name
+
+ def password(self):
+ # To allow all users to get db_password we have to call it with sudo
+ password = self.env['ir.config_parameter'].sudo().get_param(
+ 'db_password') or ''
+ return tools.config['db_password'] or password
+
+ def execute_report(self, data_file, output_file, sub_report_data_files):
+ locale = self.context.get('lang', 'en_US')
+ connection_parameters = {
+ 'output': self.output_format,
+ 'csv': data_file,
+ 'dsn': self.dsn(),
+ 'user': self.user_name(),
+ 'password': self.password(),
+ 'subreports': sub_report_data_files,
+ }
+ parameters = {
+ 'STANDARD_DIR': self.report.standard_directory(),
+ 'REPORT_LOCALE': locale,
+ 'IDS': self.ids,
+ }
+ if 'parameters' in self.data:
+ parameters.update(json.loads(self.data.get('parameters')))
+ server = JasperServer(int(tools.config['jasperport']))
+ company_rec = self.env['res.users'].browse(self.uid).company_id
+ server.javapath = company_rec and company_rec.java_path or ''
+ server.pidfile = tools.config['jasperpid']
+ return server.execute(
+ connection_parameters, self.report_path, output_file, parameters)
diff --git a/jasper_reports/JasperReports/jasper_server.py b/jasper_reports/JasperReports/jasper_server.py
new file mode 100755
index 0000000..c055072
--- /dev/null
+++ b/jasper_reports/JasperReports/jasper_server.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (C) 2013 Tadeus Prastowo <tadeus.prastowo@infi-nity.com>
+# Vikasa Infinity Anugrah <http://www.infi-nity.com>
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+import os
+import glob
+import time
+import socket
+import subprocess
+from xmlrpc import client as xmlrpclib
+import logging
+
+from odoo.exceptions import UserError
+from odoo.tools.translate import _
+
+
+class JasperServer:
+
+ def __init__(self, port=8090):
+ self.port = port
+ self.pidfile = None
+ self.javapath = None
+ url = 'http://localhost:%d' % port
+ self.proxy = xmlrpclib.ServerProxy(url, allow_none=True)
+ self.logger = logging.getLogger(__name__)
+
+ def error(self, message):
+ if self.logger:
+ self.logger.error("%s" % message)
+
+ def path(self):
+ return os.path.abspath(os.path.dirname(__file__))
+
+ def start(self):
+ java_path = self.javapath
+ if java_path is False:
+ raise UserError(_('Java Path Not Found !\n'
+ 'Please add java path into the jasper '
+ 'configuration page under the company form '
+ 'view'))
+ else:
+ libraries = str(java_path) + '/lib'
+ if os.path.exists(str(libraries)):
+ self.javapath = java_path
+ else:
+ raise UserError(_('libraries Not Found !\n'
+ 'No libraries found in Java'))
+
+ env = {}
+ env.update(os.environ)
+ if os.name == 'nt':
+ a = ';'
+ else:
+ a = ':'
+ libs = os.path.join(self.path(), '..', 'java', 'lib', '*.jar')
+ env['CLASSPATH'] = os.path.join(self.path(), '..', 'java' + a) + \
+ a.join(glob.glob(libs)) + a + os.path.join(
+ self.path(), '..', 'custom_reports')
+
+ cwd = os.path.join(self.path(), '..', 'java')
+
+ # Set headless = True because otherwise, java may use
+ # existing X session and if session is closed JasperServer
+ # would start throwing exceptions. So we better avoid
+ # using the session at all.
+ command = ['java', '-Djava.awt.headless=true',
+ '-XX:MaxHeapSize=512m',
+ '-XX:InitialHeapSize=512m',
+ '-XX:CompressedClassSpaceSize=64m',
+ '-XX:MaxMetaspaceSize=128m',
+ # '-XX:+UseConcMarkSweepGC', ### OpenJDK 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
+ 'com.nantic.jasperreports.JasperServer',
+ str(self.port)]
+ process = subprocess.Popen(command, env=env, cwd=cwd)
+
+ if self.pidfile:
+ with open(self.pidfile, 'w') as f:
+ f.write(str(process.pid))
+
+ def execute(self, *args):
+ """
+ Render report and return the number of pages generated.
+ """
+ try:
+ return self.proxy.Report.execute(*args)
+ except socket.error as e:
+ self.start()
+ for x in range(40):
+ time.sleep(1)
+ try:
+ return self.proxy.Report.execute(*args)
+ except socket.error as e:
+ self.error("EXCEPTION: %s %s" % (str(e), str(e.args)))
+ pass
+ except xmlrpclib.Fault as e:
+ raise UserError(_('Report Error\n%s') % e)
+ except xmlrpclib.Fault as e:
+ raise UserError(_('Report Error\n%s') % e)
diff --git a/jasper_reports/JasperReports/record_data_generator.py b/jasper_reports/JasperReports/record_data_generator.py
new file mode 100755
index 0000000..4b12a64
--- /dev/null
+++ b/jasper_reports/JasperReports/record_data_generator.py
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (c) 2008-2012 NaN Projectes de Programari Lliure, S.L.
+# http://www.NaN-tic.com
+# Copyright (C) 2019-Today Serpent Consulting Services Pvt. Ltd.
+# (<http://www.serpentcs.com>)
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+import csv
+import codecs
+from xml.dom.minidom import getDOMImplementation
+
+from .abstract_data_generator import AbstractDataGenerator
+
+
+class CsvRecordDataGenerator(AbstractDataGenerator):
+
+ def __init__(self, report, records):
+ self.report = report
+ self.records = records
+ self.temporaryFiles = []
+
+ # CSV file generation using a list of dictionaries provided by
+ # the parser function.
+ def generate(self, file_name):
+
+ with open(file_name, 'wb+') as f:
+ csv.QUOTE_ALL = True
+ field_names = self.report.field_names
+ # JasperReports CSV reader requires an extra colon
+ # at the end of the line.
+ writer = csv.DictWriter(
+ f, field_names + [''], delimiter=',', quotechar='"')
+ header = {}
+
+ for field in field_names + ['']:
+ header[field] = field
+
+ writer.writerow(header)
+ error_reported_fields = []
+
+ for record in self.records:
+
+ row = {}
+ for field in record:
+ if field not in self.report.fields:
+ if field not in error_reported_fields:
+ error_reported_fields.append(field)
+ continue
+
+ value = record.get(field, False)
+ if value is False:
+ value = ''
+ elif isinstance(value, str):
+ value = value.encode('utf-8')
+ elif isinstance(value, float):
+ value = '%.10f' % value
+ elif not isinstance(value, str):
+ value = str(value)
+ row[self.report.fields[field]['name']] = value
+
+ writer.writerow(row)
+
+
+class XmlRecordDataGenerator(AbstractDataGenerator):
+
+ def __init__(self):
+ super(XmlRecordDataGenerator, self).__init__()
+ self.document = None
+
+ # XML file generation using a list of dictionaries provided by
+ # the parser function.
+ def generate(self, file_name):
+
+ # Once all records have been calculated, create the XML structure
+ self.document = getDOMImplementation().createDocument(
+ None, 'data', None)
+ top_node = self.document.documentElement
+
+ for record in self.data['records']:
+ record_node = self.document.createElement('record')
+ top_node.appendChild(record_node)
+
+ for field, value in record.iteritems():
+ field_node = self.document.createElement(field)
+ record_node.appendChild(field_node)
+ # The rest of field types must be converted into str
+ if value is False:
+ value = ''
+ elif isinstance(value, str):
+ value = str(value, 'utf-8')
+ elif isinstance(value, float):
+ value = '%.10f' % value
+ elif not isinstance(value, str):
+ value = str(value)
+
+ value_node = self.document.createTextNode(value)
+ field_node.appendChild(value_node)
+
+ # Once created, the only missing step is to store the XML into a file
+ with codecs.open(file_name, 'wb+', 'utf-8') as f:
+ top_node.writexml(f)
diff --git a/jasper_reports/JasperReports/websrv_lib.py b/jasper_reports/JasperReports/websrv_lib.py
new file mode 100755
index 0000000..1c0d3c4
--- /dev/null
+++ b/jasper_reports/JasperReports/websrv_lib.py
@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright P. Christeas <p_christ@hol.gr> 2008-2010
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsibility of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# guarantees and support are strongly advised to contract a Free Software
+# Service Company
+#
+# This program is Free Software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+###############################################################################
+
+
+""" Framework for generic http servers
+
+ This library contains *no* Odoo-specific functionality. It should be
+ usable in other projects, too.
+"""
+
+import logging
+from http.server import SimpleHTTPRequestHandler
+
+_logger = logging.getLogger(__name__)
+
+# A list of HTTPDir.
+handlers = []
+
+
+class AuthRequiredExc(Exception):
+ def __init__(self, atype, realm):
+ Exception.__init__(self)
+ self.atype = atype
+ self.realm = realm
+
+
+class AuthRejectedExc(Exception):
+ pass
+
+
+class AuthProvider:
+ def __init__(self, realm):
+ self.realm = realm
+
+ def authenticate(self, user, passwd, client_address):
+ return False
+
+ def log(self, msg):
+ _logger.info(msg)
+
+ def check_request(self, handler, path='/'):
+ """ Check if we are allowed to process that request
+ """
+ pass
+
+
+class HTTPHandler(SimpleHTTPRequestHandler):
+ def __init__(self, request, client_address, server):
+ SimpleHTTPRequestHandler.__init__(
+ self, request, client_address, server)
+ self.protocol_version = 'HTTP/1.1'
+ self.connection = DummyConn()
+
+ def handle(self):
+ """ Classes here should NOT handle inside their constructor
+ """
+ pass
+
+ def finish(self):
+ pass
+
+ def setup(self):
+ pass
+
+
+class HTTPDir:
+ """ A dispatcher class, like a virtual folder in httpd
+ """
+
+ def __init__(self, path, handler, auth_provider=None, secure_only=False):
+ self.path = path
+ self.handler = handler
+ self.auth_provider = auth_provider
+ self.secure_only = secure_only
+
+ def matches(self, request):
+ """ Test if some request matches us. If so, return
+ the matched path. """
+ if request.startswith(self.path):
+ return self.path
+ return False
+
+ def instanciate_handler(self, request, client_address, server):
+ handler = self.handler(NoConnection(request), client_address, server)
+ if self.auth_provider:
+ handler.auth_provider = self.auth_provider()
+ return handler
+
+
+def reg_http_service(path, handler, auth_provider=None, secure_only=False):
+ """ Register a HTTP handler at a given path.
+
+ The auth_provider will be instanciated and set on the handler instances.
+ """
+ global handlers
+ service = HTTPDir(path, handler, auth_provider, secure_only)
+ pos = len(handlers)
+ lastpos = pos
+ while pos > 0:
+ pos -= 1
+ if handlers[pos].matches(service.path):
+ lastpos = pos
+ # we won't break here, but search all way to the top, to
+ # ensure there is no lesser entry that will shadow the one
+ # we are inserting.
+ handlers.insert(lastpos, service)
+
+
+def list_http_services(protocol=None):
+ global handlers
+ ret = []
+ for svc in handlers:
+ if protocol is None or protocol == 'http' or svc.secure_only:
+ ret.append((svc.path, str(svc.handler)))
+
+ return ret
+
+
+def find_http_service(path, secure=False):
+ global handlers
+ for vdir in handlers:
+ p = vdir.matches(path)
+ if p is False or (vdir.secure_only and not secure):
+ continue
+ return vdir
+ return None
+
+
+class NoConnection(object):
+ """ a class to use instead of the real connection
+ """
+
+ def __init__(self, realsocket=None):
+ self.__hidden_socket = realsocket
+
+ def makefile(self, mode, bufsize):
+ return None
+
+ def close(self):
+ pass
+
+ def getsockname(self):
+ """ We need to return info about the real socket that is used for the request
+ """
+ if not self.__hidden_socket:
+ raise AttributeError("No-connection class cannot tell real socket")
+ return self.__hidden_socket.getsockname()
+
+
+class DummyConn:
+ def shutdown(self, tru):
+ pass
+
+
+def _quote_html(html):
+ return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+
+
+class FixSendError:
+ def send_error(self, code, message=None):
+
+ try:
+ short, long = self.responses[code]
+ except KeyError:
+ short, long = '???', '???'
+ if message is None:
+ message = short
+ explain = long
+ _logger.error("code %d, message %s", code, message)
+ content = (self.error_message_format % {
+ 'code': code,
+ 'message': _quote_html(message),
+ 'explain': explain
+ })
+ self.send_response(code, message)
+ self.send_header("Content-Type", self.error_content_type)
+ self.send_header('Connection', 'close')
+ self.send_header('Content-Length', len(content) or 0)
+ self.end_headers()
+ if hasattr(self, '_flush'):
+ self._flush()
+ if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
+ self.wfile.write(content)
+
+
+class HttpOptions:
+
+ _HTTP_OPTIONS = {'Allow': ['OPTIONS']}
+
+ def do_OPTIONS(self):
+ """return the list of capabilities """
+
+ opts = self._HTTP_OPTIONS
+ nopts = self._prep_OPTIONS(opts)
+ if nopts:
+ opts = nopts
+
+ self.send_response(200)
+ self.send_header("Content-Length", 0)
+ if 'Microsoft' in self.headers.get('User-Agent', ''):
+ self.send_header('MS-Author-Via', 'DAV')
+ # Microsoft's webdav lib ass-umes that the server would
+ # be a FrontPage(tm) one, unless we send a non-standard
+ # header that we are not an elephant.
+ # http://www.ibm.com/developerworks/rational/library/2089.html
+
+ for key, value in opts.items():
+ if isinstance(value, str):
+ self.send_header(key, value)
+ elif isinstance(value, (tuple, list)):
+ self.send_header(key, ', '.join(value))
+ self.end_headers()
+
+ def _prep_OPTIONS(self, opts):
+ """ Prepare the OPTIONS response, if needed.
+ Sometimes, like in special DAV folders, the OPTIONS may
+ contain extra keywords, perhaps also dependant on the
+ request url.
+ :param opts: MUST be copied before being altered
+ :returns: the updated options.
+ """
+ return opts