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