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