[MERGE] upstream
[odoo/odoo.git] / openerp / report / interface.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import os
23 import re
24
25 from lxml import etree
26
27 import openerp
28
29 import openerp
30 import openerp.tools as tools
31 import openerp.modules
32 import print_xml
33 import render
34 import urllib
35
36 #
37 # coerce any type to a unicode string (to preserve non-ascii characters)
38 # and escape XML entities
39 #
40 def toxml(value):
41     unicode_value = tools.ustr(value)
42     return unicode_value.replace('&', '&amp;').replace('<','&lt;').replace('>','&gt;')
43
44 class report_int(object):
45
46     _reports = {}
47     
48     def __init__(self, name, register=True):
49         if register:
50             assert openerp.conf.deprecation.allow_report_int_registration
51             assert name.startswith('report.'), 'Report names should start with "report.".'
52             assert name not in self._reports, 'The report "%s" already exists.' % name
53             self._reports[name] = self
54         else:
55             # The report is instanciated at each use site, which is ok.
56             pass
57
58         self.__name = name
59
60         self.name = name
61         self.id = 0
62         self.name2 = '.'.join(name.split('.')[1:])
63         # TODO the reports have methods with a 'title' kwarg that is redundant with this attribute
64         self.title = None
65
66     def create(self, cr, uid, ids, datas, context=None):
67         return False
68
69 class report_rml(report_int):
70     """
71         Automatically builds a document using the transformation process:
72             XML -> DATAS -> RML -> PDF -> HTML
73         using a XSL:RML transformation
74     """
75     def __init__(self, name, table, tmpl, xsl, register=True):
76         super(report_rml, self).__init__(name, register=register)
77         self.table = table
78         self.internal_header=False
79         self.tmpl = tmpl
80         self.xsl = xsl
81         self.bin_datas = {}
82         self.generators = {
83             'pdf': self.create_pdf,
84             'html': self.create_html,
85             'raw': self.create_raw,
86             'sxw': self.create_sxw,
87             'txt': self.create_txt,
88             'odt': self.create_odt,
89             'html2html' : self.create_html2html,
90             'makohtml2html' :self.create_makohtml2html,
91         }
92
93     def create(self, cr, uid, ids, datas, context):
94         xml = self.create_xml(cr, uid, ids, datas, context)
95         xml = tools.ustr(xml).encode('utf8')
96         report_type = datas.get('report_type', 'pdf')
97         if report_type == 'raw':
98             return xml, report_type
99         rml = self.create_rml(cr, xml, uid, context)
100         registry = openerp.registry(cr.dbname)
101         ir_actions_report_xml_obj = registry['ir.actions.report.xml']
102         report_xml_ids = ir_actions_report_xml_obj.search(cr, uid, [('report_name', '=', self.name[7:])], context=context)
103         self.title = report_xml_ids and ir_actions_report_xml_obj.browse(cr,uid,report_xml_ids)[0].name or 'OpenERP Report'
104         create_doc = self.generators[report_type]
105         pdf = create_doc(rml, title=self.title)
106         return pdf, report_type
107
108     def create_xml(self, cr, uid, ids, datas, context=None):
109         if not context:
110             context={}
111         doc = print_xml.document(cr, uid, datas, {})
112         self.bin_datas.update( doc.bin_datas  or {})
113         doc.parse(self.tmpl, ids, self.table, context)
114         xml = doc.xml_get()
115         doc.close()
116         return self.post_process_xml_data(cr, uid, xml, context)
117
118     def post_process_xml_data(self, cr, uid, xml, context=None):
119
120         if not context:
121             context={}
122         # find the position of the 3rd tag
123         # (skip the <?xml ...?> and the "root" tag)
124         iter = re.finditer('<[^>]*>', xml)
125         i = iter.next()
126         i = iter.next()
127         pos_xml = i.end()
128
129         doc = print_xml.document(cr, uid, {}, {})
130         tmpl_path = openerp.modules.get_module_resource('base', 'report', 'corporate_defaults.xml')
131         doc.parse(tmpl_path, [uid], 'res.users', context)
132         corporate_header = doc.xml_get()
133         doc.close()
134
135         # find the position of the tag after the <?xml ...?> tag
136         iter = re.finditer('<[^>]*>', corporate_header)
137         i = iter.next()
138         pos_header = i.end()
139
140         return xml[:pos_xml] + corporate_header[pos_header:] + xml[pos_xml:]
141
142     #
143     # TODO: The translation doesn't work for "<tag t="1">textext<tag> tex</tag>text</tag>"
144     #
145     def create_rml(self, cr, xml, uid, context=None):
146         if self.tmpl=='' and not self.internal_header:
147             self.internal_header=True
148         if not context:
149             context={}
150         registry = openerp.registry(cr.dbname)
151         ir_translation_obj = registry['ir.translation']
152
153         # In some case we might not use xsl ...
154         if not self.xsl:
155             return xml
156
157         stylesheet_file = tools.file_open(self.xsl)
158         try:
159             stylesheet = etree.parse(stylesheet_file)
160             xsl_path, _ = os.path.split(self.xsl)
161             for import_child in stylesheet.findall('./import'):
162                 if 'href' in import_child.attrib:
163                     imp_file = import_child.get('href')
164                     _, imp_file = tools.file_open(imp_file, subdir=xsl_path, pathinfo=True)
165                     import_child.set('href', urllib.quote(str(imp_file)))
166                     imp_file.close()
167         finally:
168             stylesheet_file.close()
169
170         #TODO: get all the translation in one query. That means we have to:
171         # * build a list of items to translate,
172         # * issue the query to translate them,
173         # * (re)build/update the stylesheet with the translated items
174
175         def translate(doc, lang):
176             translate_aux(doc, lang, False)
177
178         def translate_aux(doc, lang, t):
179             for node in doc:
180                 t = t or node.get("t")
181                 if t:
182                     text = None
183                     tail = None
184                     if node.text:
185                         text = node.text.strip().replace('\n',' ')
186                     if node.tail:
187                         tail = node.tail.strip().replace('\n',' ')
188                     if text:
189                         translation1 = ir_translation_obj._get_source(cr, uid, self.name2, 'xsl', lang, text)
190                         if translation1:
191                             node.text = node.text.replace(text, translation1)
192                     if tail:
193                         translation2 = ir_translation_obj._get_source(cr, uid, self.name2, 'xsl', lang, tail)
194                         if translation2:
195                             node.tail = node.tail.replace(tail, translation2)
196                 translate_aux(node, lang, t)
197
198         if context.get('lang', False):
199             translate(stylesheet.iter(), context['lang'])
200
201         transform = etree.XSLT(stylesheet)
202         xml = etree.tostring(
203             transform(etree.fromstring(xml)))
204
205         return xml
206
207     def create_pdf(self, rml, localcontext = None, logo=None, title=None):
208         if not localcontext:
209             localcontext = {}
210         localcontext.update({'internal_header':self.internal_header})
211         if logo:
212             self.bin_datas['logo'] = logo
213         else:
214             if 'logo' in self.bin_datas:
215                 del self.bin_datas['logo']
216         obj = render.rml(rml, localcontext, self.bin_datas, self._get_path(), title)
217         obj.render()
218         return obj.get()
219
220     def create_html(self, rml, localcontext = None, logo=None, title=None):
221         obj = render.rml2html(rml, localcontext, self.bin_datas)
222         obj.render()
223         return obj.get()
224
225     def create_txt(self, rml,localcontext, logo=None, title=None):
226         obj = render.rml2txt(rml, localcontext, self.bin_datas)
227         obj.render()
228         return obj.get().encode('utf-8')
229
230     def create_html2html(self, rml, localcontext = None, logo=None, title=None):
231         obj = render.html2html(rml, localcontext, self.bin_datas)
232         obj.render()
233         return obj.get()
234
235
236     def create_raw(self,rml, localcontext = None, logo=None, title=None):
237         obj = render.odt2odt(etree.XML(rml),localcontext)
238         obj.render()
239         return etree.tostring(obj.get())
240
241     def create_sxw(self,rml,localcontext = None):
242         obj = render.odt2odt(rml,localcontext)
243         obj.render()
244         return obj.get()
245
246     def create_odt(self,rml,localcontext = None):
247         obj = render.odt2odt(rml,localcontext)
248         obj.render()
249         return obj.get()
250
251     def create_makohtml2html(self,html,localcontext = None):
252         obj = render.makohtml2html(html,localcontext)
253         obj.render()
254         return obj.get()
255
256     def _get_path(self):
257         return [
258             self.tmpl.replace(os.path.sep, '/').rsplit('/', 1)[0],
259             'addons',
260             tools.config['root_path']
261         ]
262
263 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: