[MERGE] latest trunk
[odoo/odoo.git] / openerp / report / report_sxw.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 from lxml import etree
22 import StringIO
23 import cStringIO
24 import base64
25 from datetime import datetime
26 import os
27 import re
28 import time
29 from interface import report_rml
30 import preprocess
31 import logging
32 import openerp.pooler as pooler
33 import openerp.tools as tools
34 import zipfile
35 import common
36 from openerp.osv.fields import float as float_class, function as function_class
37 from openerp.osv.orm import browse_record
38 from openerp.tools.translate import _
39
40 DT_FORMAT = '%Y-%m-%d'
41 DHM_FORMAT = '%Y-%m-%d %H:%M:%S'
42 HM_FORMAT = '%H:%M:%S'
43
44 rml_parents = {
45     'tr':1,
46     'li':1,
47     'story': 0,
48     'section': 0
49 }
50
51 rml_tag="para"
52
53 sxw_parents = {
54     'table-row': 1,
55     'list-item': 1,
56     'body': 0,
57     'section': 0,
58 }
59
60 html_parents = {
61     'tr' : 1,
62     'body' : 0,
63     'div' : 0
64     }
65 sxw_tag = "p"
66
67 rml2sxw = {
68     'para': 'p',
69 }
70
71 def get_date_length(date_format=DT_FORMAT):
72     return len((datetime.now()).strftime(date_format))
73
74 class _format(object):
75     def set_value(self, cr, uid, name, object, field, lang_obj):
76         self.object = object
77         self._field = field
78         self.name = name
79         self.lang_obj = lang_obj
80
81 class _float_format(float, _format):
82     def __init__(self,value):
83         super(_float_format, self).__init__()
84         self.val = value or 0.0
85
86     def __str__(self):
87         digits = 2
88         if hasattr(self,'_field') and getattr(self._field, 'digits', None):
89             digits = self._field.digits[1]
90         if hasattr(self, 'lang_obj'):
91             return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True)
92         return str(self.val)
93
94 class _int_format(int, _format):
95     def __init__(self,value):
96         super(_int_format, self).__init__()
97         self.val = value or 0
98
99     def __str__(self):
100         if hasattr(self,'lang_obj'):
101             return self.lang_obj.format('%.d', self.name, True)
102         return str(self.val)
103
104 class _date_format(str, _format):
105     def __init__(self,value):
106         super(_date_format, self).__init__()
107         self.val = value and str(value) or ''
108
109     def __str__(self):
110         if self.val:
111             if getattr(self,'name', None):
112                 date = datetime.strptime(self.name[:get_date_length()], DT_FORMAT)
113                 return date.strftime(str(self.lang_obj.date_format))
114         return self.val
115
116 class _dttime_format(str, _format):
117     def __init__(self,value):
118         super(_dttime_format, self).__init__()
119         self.val = value and str(value) or ''
120
121     def __str__(self):
122         if self.val and getattr(self,'name', None):
123             return datetime.strptime(self.name, DHM_FORMAT)\
124                    .strftime("%s %s"%(str(self.lang_obj.date_format),
125                                       str(self.lang_obj.time_format)))
126         return self.val
127
128
129 _fields_process = {
130     'float': _float_format,
131     'date': _date_format,
132     'integer': _int_format,
133     'datetime' : _dttime_format
134 }
135
136 #
137 # Context: {'node': node.dom}
138 #
139 class browse_record_list(list):
140     def __init__(self, lst, context):
141         super(browse_record_list, self).__init__(lst)
142         self.context = context
143
144     def __getattr__(self, name):
145         res = browse_record_list([getattr(x,name) for x in self], self.context)
146         return res
147
148     def __str__(self):
149         return "browse_record_list("+str(len(self))+")"
150
151 class rml_parse(object):
152     def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None):
153         if not context:
154             context={}
155         self.cr = cr
156         self.uid = uid
157         self.pool = pooler.get_pool(cr.dbname)
158         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
159         self.localcontext = {
160             'user': user,
161             'setCompany': self.setCompany,
162             'repeatIn': self.repeatIn,
163             'setLang': self.setLang,
164             'setTag': self.setTag,
165             'removeParentNode': self.removeParentNode,
166             'format': self.format,
167             'formatLang': self.formatLang,
168             'lang' : user.company_id.partner_id.lang,
169             'translate' : self._translate,
170             'setHtmlImage' : self.set_html_image,
171             'strip_name' : self._strip_name,
172             'time' : time,
173             'display_address': self.display_address,
174             # more context members are setup in setCompany() below:
175             #  - company_id
176             #  - logo
177         }
178         self.setCompany(user.company_id)
179         self.localcontext.update(context)
180         self.name = name
181         self._node = None
182         self.parents = parents
183         self.tag = tag
184         self._lang_cache = {}
185         self.lang_dict = {}
186         self.default_lang = {}
187         self.lang_dict_called = False
188         self._transl_regex = re.compile('(\[\[.+?\]\])')
189
190     def setTag(self, oldtag, newtag, attrs=None):
191         return newtag, attrs
192
193     def _ellipsis(self, char, size=100, truncation_str='...'):
194         if len(char) <= size:
195             return char
196         return char[:size-len(truncation_str)] + truncation_str
197
198     def setCompany(self, company_id):
199         if company_id:
200             self.localcontext['company'] = company_id
201             self.localcontext['logo'] = company_id.logo
202             self.rml_header = company_id.rml_header
203             self.rml_header2 = company_id.rml_header2
204             self.rml_header3 = company_id.rml_header3
205             self.logo = company_id.logo
206
207     def _strip_name(self, name, maxlen=50):
208         return self._ellipsis(name, maxlen)
209
210     def format(self, text, oldtag=None):
211         return text.strip()
212
213     def removeParentNode(self, tag=None):
214         raise GeneratorExit('Skip')
215
216     def set_html_image(self,id,model=None,field=None,context=None):
217         if not id :
218             return ''
219         if not model:
220             model = 'ir.attachment'
221         try :
222             id = int(id)
223             res = self.pool.get(model).read(self.cr,self.uid,id)
224             if field :
225                 return res[field]
226             elif model =='ir.attachment' :
227                 return res['datas']
228             else :
229                 return ''
230         except Exception:
231             return ''
232
233     def setLang(self, lang):
234         self.localcontext['lang'] = lang
235         self.lang_dict_called = False
236         for obj in self.objects:
237             obj._context['lang'] = lang
238
239     def _get_lang_dict(self):
240         pool_lang = self.pool.get('res.lang')
241         lang = self.localcontext.get('lang', 'en_US') or 'en_US'
242         lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0]
243         lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids)
244         self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format})
245         self.default_lang[lang] = self.lang_dict.copy()
246         return True
247
248     def digits_fmt(self, obj=None, f=None, dp=None):
249         digits = self.get_digits(obj, f, dp)
250         return "%%.%df" % (digits, )
251
252     def get_digits(self, obj=None, f=None, dp=None):
253         d = DEFAULT_DIGITS = 2
254         if dp:
255             decimal_precision_obj = self.pool.get('decimal.precision')
256             ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)])
257             if ids:
258                 d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits
259         elif obj and f:
260             res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
261             if isinstance(res_digits, tuple):
262                 d = res_digits[1]
263             else:
264                 d = res_digits(self.cr)[1]
265         elif (hasattr(obj, '_field') and\
266                 isinstance(obj._field, (float_class, function_class)) and\
267                 obj._field.digits):
268                 d = obj._field.digits[1] or DEFAULT_DIGITS
269         return d
270
271     def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
272         """
273             Assuming 'Account' decimal.precision=3:
274                 formatLang(value) -> digits=2 (default)
275                 formatLang(value, digits=4) -> digits=4
276                 formatLang(value, dp='Account') -> digits=3
277                 formatLang(value, digits=5, dp='Account') -> digits=5
278         """
279         if digits is None:
280             if dp:
281                 digits = self.get_digits(dp=dp)
282             else:
283                 digits = self.get_digits(value)
284
285         if isinstance(value, (str, unicode)) and not value:
286             return ''
287
288         if not self.lang_dict_called:
289             self._get_lang_dict()
290             self.lang_dict_called = True
291
292         if date or date_time:
293             if not str(value):
294                 return ''
295
296             date_format = self.lang_dict['date_format']
297             parse_format = DT_FORMAT
298             if date_time:
299                 value=value.split('.')[0]
300                 date_format = date_format + " " + self.lang_dict['time_format']
301                 parse_format = DHM_FORMAT
302             if not isinstance(value, time.struct_time):
303                 return time.strftime(date_format, time.strptime(value[:get_date_length(parse_format)], parse_format))
304
305             else:
306                 date = datetime(*value.timetuple()[:6])
307             return date.strftime(date_format)
308
309         res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
310         if currency_obj:
311             if currency_obj.position == 'after':
312                 res='%s %s'%(res,currency_obj.symbol)
313             elif currency_obj and currency_obj.position == 'before':
314                 res='%s %s'%(currency_obj.symbol, res)
315         return res
316
317     def display_address(self, address_browse_record):
318         return self.pool.get('res.partner.address')._display_address(self.cr, self.uid, address_browse_record)
319
320     def repeatIn(self, lst, name,nodes_parent=False):
321         ret_lst = []
322         for id in lst:
323             ret_lst.append({name:id})
324         return ret_lst
325
326     def _translate(self,text):
327         lang = self.localcontext['lang']
328         if lang and text and not text.isspace():
329             transl_obj = self.pool.get('ir.translation')
330             piece_list = self._transl_regex.split(text)
331             for pn in range(len(piece_list)):
332                 if not self._transl_regex.match(piece_list[pn]):
333                     source_string = piece_list[pn].replace('\n', ' ').strip()
334                     if len(source_string):
335                         translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string)
336                         if translated_string:
337                             piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
338             text = ''.join(piece_list)
339         return text
340
341     def _add_header(self, rml_dom, header='external'):
342         if header=='internal':
343             rml_head =  self.rml_header2
344         elif header=='internal landscape':
345             rml_head =  self.rml_header3
346         else:
347             rml_head =  self.rml_header
348
349         head_dom = etree.XML(rml_head)
350         for tag in head_dom:
351             found = rml_dom.find('.//'+tag.tag)
352             if found is not None and len(found):
353                 if tag.get('position'):
354                     found.append(tag)
355                 else :
356                     found.getparent().replace(found,tag)
357         return True
358
359     def set_context(self, objects, data, ids, report_type = None):
360         self.localcontext['data'] = data
361         self.localcontext['objects'] = objects
362         self.localcontext['digits_fmt'] = self.digits_fmt
363         self.localcontext['get_digits'] = self.get_digits
364         self.datas = data
365         self.ids = ids
366         self.objects = objects
367         if report_type:
368             if report_type=='odt' :
369                 self.localcontext.update({'name_space' :common.odt_namespace})
370             else:
371                 self.localcontext.update({'name_space' :common.sxw_namespace})
372
373         # WARNING: the object[0].exists() call below is slow but necessary because
374         # some broken reporting wizards pass incorrect IDs (e.g. ir.ui.menu ids)
375         if objects and len(objects) == 1 and \
376             objects[0].exists() and 'company_id' in objects[0] and objects[0].company_id:
377             # When we print only one record, we can auto-set the correct
378             # company in the localcontext. For other cases the report
379             # will have to call setCompany() inside the main repeatIn loop.
380             self.setCompany(objects[0].company_id)
381
382 class report_sxw(report_rml, preprocess.report):
383     def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False):
384         report_rml.__init__(self, name, table, rml, '')
385         self.name = name
386         self.parser = parser
387         self.header = header
388         self.store = store
389         self.internal_header=False
390         if header=='internal' or header=='internal landscape':
391             self.internal_header=True
392
393     def getObjects(self, cr, uid, ids, context):
394         table_obj = pooler.get_pool(cr.dbname).get(self.table)
395         return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
396
397     def create(self, cr, uid, ids, data, context=None):
398         if context is None:
399             context = {}
400         if self.internal_header:
401             context.update(internal_header=self.internal_header)
402         # skip osv.fields.sanitize_binary_value() because we want the raw bytes in all cases
403         context.update(bin_raw=True)
404         pool = pooler.get_pool(cr.dbname)
405         ir_obj = pool.get('ir.actions.report.xml')
406         report_xml_ids = ir_obj.search(cr, uid,
407                 [('report_name', '=', self.name[7:])], context=context)
408         if report_xml_ids:
409             report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context)
410         else:
411             title = ''
412             report_file = tools.file_open(self.tmpl, subdir=None)
413             try:
414                 rml = report_file.read()
415                 report_type= data.get('report_type', 'pdf')
416                 class a(object):
417                     def __init__(self, *args, **argv):
418                         for key,arg in argv.items():
419                             setattr(self, key, arg)
420                 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
421             finally:
422                 report_file.close()
423         if report_xml.header:
424             report_xml.header = self.header
425         report_type = report_xml.report_type
426         if report_type in ['sxw','odt']:
427             fnct = self.create_source_odt
428         elif report_type in ['pdf','raw','txt','html']:
429             fnct = self.create_source_pdf
430         elif report_type=='html2html':
431             fnct = self.create_source_html2html
432         elif report_type=='mako2html':
433             fnct = self.create_source_mako2html
434         else:
435             raise NotImplementedError(_('Unknown report type: %s') % report_type)
436         fnct_ret = fnct(cr, uid, ids, data, report_xml, context)
437         if not fnct_ret:
438             return (False,False)
439         return fnct_ret
440
441     def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
442         return self.create_single_odt(cr, uid, ids, data, report_xml, context or {})
443
444     def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None):
445         return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {})
446
447     def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None):
448         return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {})
449
450     def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None):
451         if not context:
452             context={}
453         pool = pooler.get_pool(cr.dbname)
454         attach = report_xml.attachment
455         if attach:
456             objs = self.getObjects(cr, uid, ids, context)
457             results = []
458             for obj in objs:
459                 aname = eval(attach, {'object':obj, 'time':time})
460                 result = False
461                 if report_xml.attachment_use and aname and context.get('attachment_use', True):
462                     aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
463                     if aids:
464                         brow_rec = pool.get('ir.attachment').browse(cr, uid, aids[0])
465                         if not brow_rec.datas:
466                             continue
467                         d = base64.decodestring(brow_rec.datas)
468                         results.append((d,'pdf'))
469                         continue
470                 result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
471                 if not result:
472                     return False
473                 if aname:
474                     try:
475                         name = aname+'.'+result[1]
476                         pool.get('ir.attachment').create(cr, uid, {
477                             'name': aname,
478                             'datas': base64.encodestring(result[0]),
479                             'datas_fname': name,
480                             'res_model': self.table,
481                             'res_id': obj.id,
482                             }, context=context
483                         )
484                     except Exception:
485                         #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632
486                         logging.getLogger('report').error('Could not create saved report attachment', exc_info=True)
487                 results.append(result)
488             if results:
489                 if results[0][1]=='pdf':
490                     from pyPdf import PdfFileWriter, PdfFileReader
491                     output = PdfFileWriter()
492                     for r in results:
493                         reader = PdfFileReader(cStringIO.StringIO(r[0]))
494                         for page in range(reader.getNumPages()):
495                             output.addPage(reader.getPage(page))
496                     s = cStringIO.StringIO()
497                     output.write(s)
498                     return s.getvalue(), results[0][1]
499         return self.create_single_pdf(cr, uid, ids, data, report_xml, context)
500
501     def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
502         if not context:
503             context={}
504         logo = None
505         context = context.copy()
506         title = report_xml.name
507         rml = report_xml.report_rml_content
508         # if no rml file is found
509         if not rml:
510             return False
511         rml_parser = self.parser(cr, uid, self.name2, context=context)
512         objs = self.getObjects(cr, uid, ids, context)
513         rml_parser.set_context(objs, data, ids, report_xml.report_type)
514         processed_rml = etree.XML(rml)
515         if report_xml.header:
516             rml_parser._add_header(processed_rml, self.header)
517         processed_rml = self.preprocess_rml(processed_rml,report_xml.report_type)
518         if rml_parser.logo:
519             logo = base64.decodestring(rml_parser.logo)
520         create_doc = self.generators[report_xml.report_type]
521         pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8'))
522         return (pdf, report_xml.report_type)
523
524     def create_single_odt(self, cr, uid, ids, data, report_xml, context=None):
525         if not context:
526             context={}
527         context = context.copy()
528         report_type = report_xml.report_type
529         context['parents'] = sxw_parents
530         binary_report_content = report_xml.report_sxw_content
531         if isinstance(report_xml.report_sxw_content, unicode):
532             # if binary content was passed as unicode, we must
533             # re-encode it as a 8-bit string using the pass-through
534             # 'latin1' encoding, to restore the original byte values.
535             # See also osv.fields.sanitize_binary_value()
536             binary_report_content = report_xml.report_sxw_content.encode("latin1")
537
538         sxw_io = StringIO.StringIO(binary_report_content)
539         sxw_z = zipfile.ZipFile(sxw_io, mode='r')
540         rml = sxw_z.read('content.xml')
541         meta = sxw_z.read('meta.xml')
542         mime_type = sxw_z.read('mimetype')
543         if mime_type == 'application/vnd.sun.xml.writer':
544             mime_type = 'sxw'
545         else :
546             mime_type = 'odt'
547         sxw_z.close()
548
549         rml_parser = self.parser(cr, uid, self.name2, context=context)
550         rml_parser.parents = sxw_parents
551         rml_parser.tag = sxw_tag
552         objs = self.getObjects(cr, uid, ids, context)
553         rml_parser.set_context(objs, data, ids, mime_type)
554
555         rml_dom_meta = node = etree.XML(meta)
556         elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined")
557         for pe in elements:
558             if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"):
559                 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3":
560                     pe[0].text=data['id']
561                 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4":
562                     pe[0].text=data['model']
563         meta = etree.tostring(rml_dom_meta, encoding='utf-8',
564                               xml_declaration=True)
565
566         rml_dom =  etree.XML(rml)
567         elements = []
568         key1 = rml_parser.localcontext['name_space']["text"]+"p"
569         key2 = rml_parser.localcontext['name_space']["text"]+"drop-down"
570         for n in rml_dom.iterdescendants():
571             if n.tag == key1:
572                 elements.append(n)
573         if mime_type == 'odt':
574             for pe in elements:
575                 e = pe.findall(key2)
576                 for de in e:
577                     pp=de.getparent()
578                     if de.text or de.tail:
579                         pe.text = de.text or de.tail
580                     for cnd in de:
581                         if cnd.text or cnd.tail:
582                             if pe.text:
583                                 pe.text +=  cnd.text or cnd.tail
584                             else:
585                                 pe.text =  cnd.text or cnd.tail
586                             pp.remove(de)
587         else:
588             for pe in elements:
589                 e = pe.findall(key2)
590                 for de in e:
591                     pp = de.getparent()
592                     if de.text or de.tail:
593                         pe.text = de.text or de.tail
594                     for cnd in de:
595                         text = cnd.get("{http://openoffice.org/2000/text}value",False)
596                         if text:
597                             if pe.text and text.startswith('[['):
598                                 pe.text +=  text
599                             elif text.startswith('[['):
600                                 pe.text =  text
601                             if de.getparent():
602                                 pp.remove(de)
603
604         rml_dom = self.preprocess_rml(rml_dom, mime_type)
605         create_doc = self.generators[mime_type]
606         odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
607                              encoding='utf-8', xml_declaration=True)
608         sxw_z = zipfile.ZipFile(sxw_io, mode='a')
609         sxw_z.writestr('content.xml', odt)
610         sxw_z.writestr('meta.xml', meta)
611
612         if report_xml.header:
613             #Add corporate header/footer
614             rml_file = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type))
615             try:
616                 rml = rml_file.read()
617                 rml_parser = self.parser(cr, uid, self.name2, context=context)
618                 rml_parser.parents = sxw_parents
619                 rml_parser.tag = sxw_tag
620                 objs = self.getObjects(cr, uid, ids, context)
621                 rml_parser.set_context(objs, data, ids, report_xml.report_type)
622                 rml_dom = self.preprocess_rml(etree.XML(rml),report_type)
623                 create_doc = self.generators[report_type]
624                 odt = create_doc(rml_dom,rml_parser.localcontext)
625                 if report_xml.header:
626                     rml_parser._add_header(odt)
627                 odt = etree.tostring(odt, encoding='utf-8',
628                                      xml_declaration=True)
629                 sxw_z.writestr('styles.xml', odt)
630             finally:
631                 rml_file.close()
632         sxw_z.close()
633         final_op = sxw_io.getvalue()
634         sxw_io.close()
635         return (final_op, mime_type)
636
637     def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
638         if not context:
639             context = {}
640         context = context.copy()
641         report_type = 'html'
642         context['parents'] = html_parents
643
644         html = report_xml.report_rml_content
645         html_parser = self.parser(cr, uid, self.name2, context=context)
646         html_parser.parents = html_parents
647         html_parser.tag = sxw_tag
648         objs = self.getObjects(cr, uid, ids, context)
649         html_parser.set_context(objs, data, ids, report_type)
650
651         html_dom =  etree.HTML(html)
652         html_dom = self.preprocess_rml(html_dom,'html2html')
653
654         create_doc = self.generators['html2html']
655         html = etree.tostring(create_doc(html_dom, html_parser.localcontext))
656
657         return (html.replace('&amp;','&').replace('&lt;', '<').replace('&gt;', '>').replace('</br>',''), report_type)
658
659     def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None):
660         mako_html = report_xml.report_rml_content
661         html_parser = self.parser(cr, uid, self.name2, context)
662         objs = self.getObjects(cr, uid, ids, context)
663         html_parser.set_context(objs, data, ids, 'html')
664         create_doc = self.generators['makohtml2html']
665         html = create_doc(mako_html,html_parser.localcontext)
666         return (html,'html')
667
668
669 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: