[FIX] stock: error on stock.partial.move creation
[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 import openerp.netsvc as netsvc
27 import openerp.pooler as pooler
28
29 import openerp.tools as tools
30 import openerp.modules
31 import print_xml
32 import render
33 import urllib
34
35 #
36 # coerce any type to a unicode string (to preserve non-ascii characters)
37 # and escape XML entities
38 #
39 def toxml(value):
40     unicode_value = tools.ustr(value)
41     return unicode_value.replace('&', '&amp;').replace('<','&lt;').replace('>','&gt;')
42
43 class report_int(netsvc.Service):
44     def __init__(self, name):
45         assert not self.exists(name), 'The report "%s" already exists!' % name
46         super(report_int, self).__init__(name)
47         if not name.startswith('report.'):
48             raise Exception('ConceptionError, bad report name, should start with "report."')
49         self.name = name
50         self.id = 0
51         self.name2 = '.'.join(name.split('.')[1:])
52         # TODO the reports have methods with a 'title' kwarg that is redundant with this attribute
53         self.title = None
54
55     def create(self, cr, uid, ids, datas, context=None):
56         return False
57
58 class report_rml(report_int):
59     """
60         Automatically builds a document using the transformation process:
61             XML -> DATAS -> RML -> PDF -> HTML
62         using a XSL:RML transformation
63     """
64     def __init__(self, name, table, tmpl, xsl):
65         super(report_rml, self).__init__(name)
66         self.table = table
67         self.internal_header=False
68         self.tmpl = tmpl
69         self.xsl = xsl
70         self.bin_datas = {}
71         self.generators = {
72             'pdf': self.create_pdf,
73             'html': self.create_html,
74             'raw': self.create_raw,
75             'sxw': self.create_sxw,
76             'txt': self.create_txt,
77             'odt': self.create_odt,
78             'html2html' : self.create_html2html,
79             'makohtml2html' :self.create_makohtml2html,
80         }
81
82     def create(self, cr, uid, ids, datas, context):
83         xml = self.create_xml(cr, uid, ids, datas, context)
84         xml = tools.ustr(xml).encode('utf8')
85         report_type = datas.get('report_type', 'pdf')
86         if report_type == 'raw':
87             return xml, report_type
88         rml = self.create_rml(cr, xml, uid, context)
89         pool = pooler.get_pool(cr.dbname)
90         ir_actions_report_xml_obj = pool.get('ir.actions.report.xml')
91         report_xml_ids = ir_actions_report_xml_obj.search(cr, uid, [('report_name', '=', self.name[7:])], context=context)
92         self.title = report_xml_ids and ir_actions_report_xml_obj.browse(cr,uid,report_xml_ids)[0].name or 'OpenERP Report'
93         create_doc = self.generators[report_type]
94         pdf = create_doc(rml, title=self.title)
95         return pdf, report_type
96
97     def create_xml(self, cr, uid, ids, datas, context=None):
98         if not context:
99             context={}
100         doc = print_xml.document(cr, uid, datas, {})
101         self.bin_datas.update( doc.bin_datas  or {})
102         doc.parse(self.tmpl, ids, self.table, context)
103         xml = doc.xml_get()
104         doc.close()
105         return self.post_process_xml_data(cr, uid, xml, context)
106
107     def post_process_xml_data(self, cr, uid, xml, context=None):
108
109         if not context:
110             context={}
111         # find the position of the 3rd tag
112         # (skip the <?xml ...?> and the "root" tag)
113         iter = re.finditer('<[^>]*>', xml)
114         i = iter.next()
115         i = iter.next()
116         pos_xml = i.end()
117
118         doc = print_xml.document(cr, uid, {}, {})
119         tmpl_path = openerp.modules.get_module_resource('base', 'report', 'corporate_defaults.xml')
120         doc.parse(tmpl_path, [uid], 'res.users', context)
121         corporate_header = doc.xml_get()
122         doc.close()
123
124         # find the position of the tag after the <?xml ...?> tag
125         iter = re.finditer('<[^>]*>', corporate_header)
126         i = iter.next()
127         pos_header = i.end()
128
129         return xml[:pos_xml] + corporate_header[pos_header:] + xml[pos_xml:]
130
131     #
132     # TODO: The translation doesn't work for "<tag t="1">textext<tag> tex</tag>text</tag>"
133     #
134     def create_rml(self, cr, xml, uid, context=None):
135         if self.tmpl=='' and not self.internal_header:
136             self.internal_header=True
137         if not context:
138             context={}
139         pool = pooler.get_pool(cr.dbname)
140         ir_translation_obj = pool.get('ir.translation')
141
142         # In some case we might not use xsl ...
143         if not self.xsl:
144             return xml
145
146         stylesheet_file = tools.file_open(self.xsl)
147         try:
148             stylesheet = etree.parse(stylesheet_file)
149             xsl_path, _ = os.path.split(self.xsl)
150             for import_child in stylesheet.findall('./import'):
151                 if 'href' in import_child.attrib:
152                     imp_file = import_child.get('href')
153                     _, imp_file = tools.file_open(imp_file, subdir=xsl_path, pathinfo=True)
154                     import_child.set('href', urllib.quote(str(imp_file)))
155                     imp_file.close()
156         finally:
157             stylesheet_file.close()
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         def translate(doc, lang):
165             translate_aux(doc, lang, False)
166
167         def translate_aux(doc, lang, t):
168             for node in doc:
169                 t = t or node.get("t")
170                 if t:
171                     text = None
172                     tail = None
173                     if node.text:
174                         text = node.text.strip().replace('\n',' ')
175                     if node.tail:
176                         tail = node.tail.strip().replace('\n',' ')
177                     if text:
178                         translation1 = ir_translation_obj._get_source(cr, uid, self.name2, 'xsl', lang, text)
179                         if translation1:
180                             node.text = node.text.replace(text, translation1)
181                     if tail:
182                         translation2 = ir_translation_obj._get_source(cr, uid, self.name2, 'xsl', lang, tail)
183                         if translation2:
184                             node.tail = node.tail.replace(tail, translation2)
185                 translate_aux(node, lang, t)
186
187         if context.get('lang', False):
188             translate(stylesheet.iter(), context['lang'])
189
190         transform = etree.XSLT(stylesheet)
191         xml = etree.tostring(
192             transform(etree.fromstring(xml)))
193
194         return xml
195
196     def create_pdf(self, rml, localcontext = None, logo=None, title=None):
197         if not localcontext:
198             localcontext = {}
199         localcontext.update({'internal_header':self.internal_header})
200         if logo:
201             self.bin_datas['logo'] = logo
202         else:
203             if 'logo' in self.bin_datas:
204                 del self.bin_datas['logo']
205         obj = render.rml(rml, localcontext, self.bin_datas, self._get_path(), title)
206         obj.render()
207         return obj.get()
208
209     def create_html(self, rml, localcontext = None, logo=None, title=None):
210         obj = render.rml2html(rml, localcontext, self.bin_datas)
211         obj.render()
212         return obj.get()
213
214     def create_txt(self, rml,localcontext, logo=None, title=None):
215         obj = render.rml2txt(rml, localcontext, self.bin_datas)
216         obj.render()
217         return obj.get().encode('utf-8')
218
219     def create_html2html(self, rml, localcontext = None, logo=None, title=None):
220         obj = render.html2html(rml, localcontext, self.bin_datas)
221         obj.render()
222         return obj.get()
223
224
225     def create_raw(self,rml, localcontext = None, logo=None, title=None):
226         obj = render.odt2odt(etree.XML(rml),localcontext)
227         obj.render()
228         return etree.tostring(obj.get())
229
230     def create_sxw(self,rml,localcontext = None):
231         obj = render.odt2odt(rml,localcontext)
232         obj.render()
233         return obj.get()
234
235     def create_odt(self,rml,localcontext = None):
236         obj = render.odt2odt(rml,localcontext)
237         obj.render()
238         return obj.get()
239
240     def create_makohtml2html(self,html,localcontext = None):
241         obj = render.makohtml2html(html,localcontext)
242         obj.render()
243         return obj.get()
244
245     def _get_path(self):
246         return [
247             self.tmpl.replace(os.path.sep, '/').rsplit('/', 1)[0],
248             'addons',
249             tools.config['root_path']
250         ]
251
252 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: