diff options
Diffstat (limited to 'jasper_reports/JasperReports')
| -rwxr-xr-x | jasper_reports/JasperReports/__init__.py | 39 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/abstract_data_generator.py | 40 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/browse_data_generator.py | 492 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/http_server.py | 102 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/jasper_report.py | 352 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/jasper_report_config.py | 260 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/jasper_server.py | 126 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/record_data_generator.py | 125 | ||||
| -rwxr-xr-x | jasper_reports/JasperReports/websrv_lib.py | 245 |
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("&", "&").replace("<", "<").replace(">", ">") + + +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 |
