[MERGE] currency position dsh
[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             # more context members are setup in setCompany() below:
174             #  - company_id
175             #  - logo
176         }
177         self.setCompany(user.company_id)
178         self.localcontext.update(context)
179         self.name = name
180         self._node = None
181         self.parents = parents
182         self.tag = tag
183         self._lang_cache = {}
184         self.lang_dict = {}
185         self.default_lang = {}
186         self.lang_dict_called = False
187         self._transl_regex = re.compile('(\[\[.+?\]\])')
188
189     def setTag(self, oldtag, newtag, attrs=None):
190         return newtag, attrs
191
192     def _ellipsis(self, char, size=100, truncation_str='...'):
193         if len(char) <= size:
194             return char
195         return char[:size-len(truncation_str)] + truncation_str
196
197     def setCompany(self, company_id):
198         if company_id:
199             self.localcontext['company'] = company_id
200             self.localcontext['logo'] = company_id.logo
201             self.rml_header = company_id.rml_header
202             self.rml_header2 = company_id.rml_header2
203             self.rml_header3 = company_id.rml_header3
204             self.logo = company_id.logo
205
206     def _strip_name(self, name, maxlen=50):
207         return self._ellipsis(name, maxlen)
208
209     def format(self, text, oldtag=None):
210         return text.strip()
211
212     def removeParentNode(self, tag=None):
213         raise GeneratorExit('Skip')
214
215     def set_html_image(self,id,model=None,field=None,context=None):
216         if not id :
217             return ''
218         if not model:
219             model = 'ir.attachment'
220         try :
221             id = int(id)
222             res = self.pool.get(model).read(self.cr,self.uid,id)
223             if field :
224                 return res[field]
225             elif model =='ir.attachment' :
226                 return res['datas']
227             else :
228                 return ''
229         except Exception:
230             return ''
231
232     def setLang(self, lang):
233         self.localcontext['lang'] = lang
234         self.lang_dict_called = False
235         for obj in self.objects:
236             obj._context['lang'] = lang
237
238     def _get_lang_dict(self):
239         pool_lang = self.pool.get('res.lang')
240         lang = self.localcontext.get('lang', 'en_US') or 'en_US'
241         lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0]
242         lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids)
243         self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format})
244         self.default_lang[lang] = self.lang_dict.copy()
245         return True
246
247     def digits_fmt(self, obj=None, f=None, dp=None):
248         digits = self.get_digits(obj, f, dp)
249         return "%%.%df" % (digits, )
250
251     def get_digits(self, obj=None, f=None, dp=None):
252         d = DEFAULT_DIGITS = 2
253         if dp:
254             decimal_precision_obj = self.pool.get('decimal.precision')
255             ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)])
256             if ids:
257                 d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits
258         elif obj and f:
259             res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
260             if isinstance(res_digits, tuple):
261                 d = res_digits[1]
262             else:
263                 d = res_digits(self.cr)[1]
264         elif (hasattr(obj, '_field') and\
265                 isinstance(obj._field, (float_class, function_class)) and\
266                 obj._field.digits):
267                 d = obj._field.digits[1] or DEFAULT_DIGITS
268         return d
269
270     def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
271         """
272             Assuming 'Account' decimal.precision=3:
273                 formatLang(value) -> digits=2 (default)
274                 formatLang(value, digits=4) -> digits=4
275                 formatLang(value, dp='Account') -> digits=3
276                 formatLang(value, digits=5, dp='Account') -> digits=5
277         """
278         if digits is None:
279             if dp:
280                 digits = self.get_digits(dp=dp)
281             else:
282                 digits = self.get_digits(value)
283
284         if isinstance(value, (str, unicode)) and not value:
285             return ''
286
287         if not self.lang_dict_called:
288             self._get_lang_dict()
289             self.lang_dict_called = True
290
291         if date or date_time:
292             if not str(value):
293                 return ''
294
295             date_format = self.lang_dict['date_format']
296             parse_format = DT_FORMAT
297             if date_time:
298                 value=value.split('.')[0]
299                 date_format = date_format + " " + self.lang_dict['time_format']
300                 parse_format = DHM_FORMAT
301             if not isinstance(value, time.struct_time):
302                 return time.strftime(date_format, time.strptime(value[:get_date_length(parse_format)], parse_format))
303
304             else:
305                 date = datetime(*value.timetuple()[:6])
306             return date.strftime(date_format)
307
308         res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
309         if currency_obj:
310             if currency_obj.position == 'after':
311                 res='%s %s'%(res,currency_obj.symbol)
312             elif currency_obj and currency_obj.position == 'before':
313                 res='%s %s'%(currency_obj.symbol, res)
314         return res
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 NotImplementedError(_('Unknown report type: %s') % 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