486ba5e5347258e19067edecf437f4aadd69885a
[odoo/odoo.git] / bin / report / interface.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import os
24 import re
25
26 import libxml2
27 import libxslt
28 from lxml import etree
29 import netsvc
30 import pooler
31
32 import tools
33 import addons
34 import print_xml
35 import render
36 import urllib
37
38 #
39 # encode a value to a string in utf8 and converts XML entities
40 #
41 def toxml(val):
42     if isinstance(val, str):
43         str_utf8 = val
44     elif isinstance(val, unicode):
45         str_utf8 = val.encode('utf-8')
46     else:
47         str_utf8 = str(val)
48     return str_utf8.replace('&', '&amp;').replace('<','&lt;').replace('>','&gt;')
49
50 class report_int(netsvc.Service):
51     def __init__(self, name, audience='*'):
52         assert not netsvc.service_exist(name), 'The report "%s" already exist!' % name
53         super(report_int, self).__init__(name, audience)
54         if name[0:7]<>'report.':
55             raise Exception, 'ConceptionError, bad report name, should start with "report."'
56         self.name = name
57         self.id = 0
58         self.name2 = '.'.join(name.split('.')[1:])
59         self.title = None
60         self.joinGroup('report')
61         self.exportMethod(self.create)
62
63     def create(self, cr, uid, ids, datas, context=None):
64         return False
65
66 """
67     Class to automatically build a document using the transformation process:
68         XML -> DATAS -> RML -> PDF
69                             -> HTML
70     using a XSL:RML transformation
71 """
72 class report_rml(report_int):
73     def __init__(self, name, table, tmpl, xsl):
74         super(report_rml, self).__init__(name)
75         self.table = table
76         self.tmpl = tmpl
77         self.xsl = xsl
78         self.bin_datas = {}
79         self.generators = {
80             'pdf': self.create_pdf,
81             'html': self.create_html,
82             'raw': self.create_raw,
83             'sxw': self.create_sxw,
84             'txt': self.create_txt,
85             'odt': self.create_odt,
86             'html2html' : self.create_html2html,
87         }
88
89     def create(self, cr, uid, ids, datas, context):
90         xml = self.create_xml(cr, uid, ids, datas, context)
91         xml = tools.ustr(xml).encode('utf8')
92         if datas.get('report_type', 'pdf') == 'raw':
93             return xml
94         rml = self.create_rml(cr, xml, uid, context)
95         pool = pooler.get_pool(cr.dbname)
96         ir_actions_report_xml_obj = pool.get('ir.actions.report.xml')
97         report_xml_ids = ir_actions_report_xml_obj.search(cr, uid, [('report_name', '=', self.name[7:])], context=context)
98         self.title = report_xml_ids and ir_actions_report_xml_obj.browse(cr,uid,report_xml_ids)[0].name or 'OpenERP Report'
99         report_type = datas.get('report_type', 'pdf')
100         create_doc = self.generators[report_type]
101         pdf = create_doc(rml, title=self.title)
102         return (pdf, report_type)
103
104     def create_xml(self, cr, uid, ids, datas, context=None):
105         if not context:
106             context={}
107         doc = print_xml.document(cr, uid, datas, {})
108         self.bin_datas.update( doc.bin_datas  or {})
109         doc.parse(self.tmpl, ids, self.table, context)
110         xml = doc.xml_get()
111         doc.close()
112         return self.post_process_xml_data(cr, uid, xml, context)
113
114     def post_process_xml_data(self, cr, uid, xml, context=None):
115         if not context:
116             context={}
117         # find the position of the 3rd tag
118         # (skip the <?xml ...?> and the "root" tag)
119         iter = re.finditer('<[^>]*>', xml)
120         i = iter.next()
121         i = iter.next()
122         pos_xml = i.end()
123
124         doc = print_xml.document(cr, uid, {}, {})
125         tmpl_path = addons.get_module_resource('base', 'report', 'corporate_defaults.xml')
126         doc.parse(tmpl_path, [uid], 'res.users', context)
127         corporate_header = doc.xml_get()
128         doc.close()
129
130         # find the position of the tag after the <?xml ...?> tag
131         iter = re.finditer('<[^>]*>', corporate_header)
132         i = iter.next()
133         pos_header = i.end()
134
135         return xml[:pos_xml] + corporate_header[pos_header:] + xml[pos_xml:]
136
137     #
138     # TODO: The translation doesn't work for "<tag t="1">textext<tag> tex</tag>text</tag>"
139     #
140     def create_rml(self, cr, xml, uid, context=None):
141         if not context:
142             context={}
143         service = netsvc.LocalService("object_proxy")
144
145         # In some case we might not use xsl ...
146         if not self.xsl:
147             return xml
148
149         # load XSL (parse it to the XML level)
150         styledoc = libxml2.parseDoc(tools.file_open(self.xsl).read())
151         xsl_path, tail = os.path.split(self.xsl)
152         for child in styledoc.children:
153             if child.name == 'import':
154                 if child.hasProp('href'):
155                     imp_file = child.prop('href')
156                     _x, imp_file = tools.file_open(imp_file, subdir=xsl_path, pathinfo=True)
157                     child.setProp('href', urllib.quote(str(imp_file)))
158
159         #TODO: get all the translation in one query. That means we have to:
160         # * build a list of items to translate,
161         # * issue the query to translate them,
162         # * (re)build/update the stylesheet with the translated items
163
164         # translate the XSL stylesheet
165         def look_down(child, lang):
166             while child is not None:
167                 if (child.type == "element") and child.hasProp('t'):
168                     #FIXME: use cursor
169                     res = service.execute(cr.dbname, uid, 'ir.translation',
170                             '_get_source', self.name2, 'xsl', lang, child.content)
171                     if res:
172                         child.setContent(res.encode('utf-8'))
173                 look_down(child.children, lang)
174                 child = child.next
175
176         if context.get('lang', False):
177             look_down(styledoc.children, context['lang'])
178
179         # parse XSL
180         style = libxslt.parseStylesheetDoc(styledoc)
181         # load XML (data)
182         doc = libxml2.parseMemory(xml,len(xml))
183         # create RML (apply XSL to XML data)
184         result = style.applyStylesheet(doc, None)
185         # save result to string
186         xml = style.saveResultToString(result)
187
188         style.freeStylesheet()
189         doc.freeDoc()
190         result.freeDoc()
191         return xml
192
193     def create_pdf(self, rml, localcontext = None, logo=None, title=None):
194         if logo:
195             self.bin_datas['logo'] = logo
196         else:
197             if 'logo' in self.bin_datas:
198                 del self.bin_datas['logo']
199         obj = render.rml(rml, localcontext, self.bin_datas, tools.config['root_path'],title)
200         obj.render()
201         return obj.get()
202
203     def create_html(self, rml, localcontext = None, logo=None, title=None):
204         obj = render.rml2html(rml, localcontext, self.bin_datas)
205         obj.render()
206         return obj.get()
207
208     def create_txt(self, xml, logo=None, title=None):
209         obj = render.rml2txt(xml, self.bin_datas)
210         obj.render()
211         return obj.get().encode('utf-8')
212
213     def create_html2html(self, rml, localcontext = None, logo=None, title=None):
214         obj = render.html2html(rml, localcontext, self.bin_datas)
215         obj.render()
216         return obj.get()
217
218
219     def create_raw(self,rml, localcontext = None, logo=None, title=None):
220         obj = render.odt2odt(etree.XML(rml),localcontext)
221         obj.render()
222         return etree.tostring(obj.get())
223
224     def create_sxw(self,rml,localcontext = None):
225         obj = render.odt2odt(rml,localcontext)
226         obj.render()
227         return obj.get()
228
229     def create_odt(self,rml,localcontext = None):
230         obj = render.odt2odt(rml,localcontext)
231         obj.render()
232         return obj.get()
233
234 from report_sxw import report_sxw
235
236 def register_all(db):
237     opj = os.path.join
238     cr = db.cursor()
239     cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,))
240     result = cr.dictfetchall()
241     cr.close()
242     for r in result:
243         if netsvc.service_exist('report.'+r['report_name']):
244             continue
245         if r['report_rml'] or r['report_rml_content_data']:
246             report_sxw('report.'+r['report_name'], r['model'],
247                     opj('addons',r['report_rml'] or '/'), header=r['header'])
248         if r['report_xsl']:
249             report_rml('report.'+r['report_name'], r['model'],
250                     opj('addons',r['report_xml']),
251                     r['report_xsl'] and opj('addons',r['report_xsl']))
252
253
254 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: