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