[MERGE] merge with latest stable
[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
234     def _get_lang_dict(self):
235         pool_lang = self.pool.get('res.lang')
236         lang = self.localcontext.get('lang', 'en_US') or 'en_US'
237         lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0]
238         lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids)
239         self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format})
240         self.default_lang[lang] = self.lang_dict.copy()
241         return True
242
243     def digits_fmt(self, obj=None, f=None, dp=None):
244         digits = self.get_digits(obj, f, dp)
245         return "%%.%df" % (digits, )
246
247     def get_digits(self, obj=None, f=None, dp=None):
248         d = DEFAULT_DIGITS = 2
249         if dp:
250             decimal_precision_obj = self.pool.get('decimal.precision')
251             ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)])
252             if ids:
253                 d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits
254         elif obj and f:
255             res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
256             if isinstance(res_digits, tuple):
257                 d = res_digits[1]
258             else:
259                 d = res_digits(self.cr)[1]
260         elif (hasattr(obj, '_field') and\
261                 isinstance(obj._field, (float_class, function_class)) and\
262                 obj._field.digits):
263                 d = obj._field.digits[1] or DEFAULT_DIGITS
264         return d
265
266     def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False):
267         """
268             Assuming 'Account' decimal.precision=3:
269                 formatLang(value) -> digits=2 (default)
270                 formatLang(value, digits=4) -> digits=4
271                 formatLang(value, dp='Account') -> digits=3
272                 formatLang(value, digits=5, dp='Account') -> digits=5
273         """
274         if digits is None:
275             if dp:
276                 digits = self.get_digits(dp=dp)
277             else:
278                 digits = self.get_digits(value)
279
280         if isinstance(value, (str, unicode)) and not value:
281             return ''
282
283         if not self.lang_dict_called:
284             self._get_lang_dict()
285             self.lang_dict_called = True
286
287         if date or date_time:
288             if not str(value):
289                 return ''
290
291             date_format = self.lang_dict['date_format']
292             parse_format = DT_FORMAT
293             if date_time:
294                 value=value.split('.')[0]
295                 date_format = date_format + " " + self.lang_dict['time_format']
296                 parse_format = DHM_FORMAT
297             if not isinstance(value, time.struct_time):
298                 return time.strftime(date_format, time.strptime(value, parse_format))
299
300             else:
301                 date = datetime(*value.timetuple()[:6])
302             return date.strftime(date_format)
303
304         return self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
305
306     def repeatIn(self, lst, name,nodes_parent=False):
307         ret_lst = []
308         for id in lst:
309             ret_lst.append({name:id})
310         return ret_lst
311
312     def _translate(self,text):
313         lang = self.localcontext['lang']
314         if lang and text and not text.isspace():
315             transl_obj = self.pool.get('ir.translation')
316             piece_list = self._transl_regex.split(text)
317             for pn in range(len(piece_list)):
318                 if not self._transl_regex.match(piece_list[pn]):
319                     source_string = piece_list[pn].replace('\n', ' ').strip()
320                     if len(source_string):
321                         translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string)
322                         if translated_string:
323                             piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
324             text = ''.join(piece_list)
325         return text
326
327     def _add_header(self, rml_dom, header='external'):
328         if header=='internal':
329             rml_head =  self.rml_header2
330         elif header=='internal landscape':
331             rml_head =  self.rml_header3
332         else:
333             rml_head =  self.rml_header
334
335         head_dom = etree.XML(rml_head)
336         for tag in head_dom:
337             found = rml_dom.find('.//'+tag.tag)
338             if found is not None and len(found):
339                 if tag.get('position'):
340                     found.append(tag)
341                 else :
342                     found.getparent().replace(found,tag)
343         return True
344
345     def set_context(self, objects, data, ids, report_type = None):
346         self.localcontext['data'] = data
347         self.localcontext['objects'] = objects
348         self.localcontext['digits_fmt'] = self.digits_fmt
349         self.localcontext['get_digits'] = self.get_digits
350         self.datas = data
351         self.ids = ids
352         self.objects = objects
353         if report_type:
354             if report_type=='odt' :
355                 self.localcontext.update({'name_space' :common.odt_namespace})
356             else:
357                 self.localcontext.update({'name_space' :common.sxw_namespace})
358
359         # WARNING: the object[0].exists() call below is slow but necessary because
360         # some broken reporting wizards pass incorrect IDs (e.g. ir.ui.menu ids)
361         if objects and len(objects) == 1 and \
362             objects[0].exists() and 'company_id' in objects[0] and objects[0].company_id:
363             # When we print only one record, we can auto-set the correct
364             # company in the localcontext. For other cases the report
365             # will have to call setCompany() inside the main repeatIn loop.
366             self.setCompany(objects[0].company_id)
367
368 class report_sxw(report_rml, preprocess.report):
369     def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False):
370         report_rml.__init__(self, name, table, rml, '')
371         self.name = name
372         self.parser = parser
373         self.header = header
374         self.store = store
375         self.internal_header=False
376         if header=='internal' or header=='internal landscape':
377             self.internal_header=True
378
379     def getObjects(self, cr, uid, ids, context):
380         table_obj = pooler.get_pool(cr.dbname).get(self.table)
381         return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
382
383     def create(self, cr, uid, ids, data, context=None):
384         if self.internal_header:
385             context.update({'internal_header':self.internal_header})
386         pool = pooler.get_pool(cr.dbname)
387         ir_obj = pool.get('ir.actions.report.xml')
388         report_xml_ids = ir_obj.search(cr, uid,
389                 [('report_name', '=', self.name[7:])], context=context)
390         if report_xml_ids:
391             report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context)
392         else:
393             title = ''
394             report_file = tools.file_open(self.tmpl, subdir=None)
395             try:
396                 rml = report_file.read()
397                 report_type= data.get('report_type', 'pdf')
398                 class a(object):
399                     def __init__(self, *args, **argv):
400                         for key,arg in argv.items():
401                             setattr(self, key, arg)
402                 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
403             finally:
404                 report_file.close()
405         if report_xml.header:
406             report_xml.header = self.header
407         report_type = report_xml.report_type
408         if report_type in ['sxw','odt']:
409             fnct = self.create_source_odt
410         elif report_type in ['pdf','raw','txt','html']:
411             fnct = self.create_source_pdf
412         elif report_type=='html2html':
413             fnct = self.create_source_html2html
414         elif report_type=='mako2html':
415             fnct = self.create_source_mako2html
416         else:
417             raise 'Unknown Report Type'
418         fnct_ret = fnct(cr, uid, ids, data, report_xml, context)
419         if not fnct_ret:
420             return (False,False)
421         return fnct_ret
422
423     def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
424         return self.create_single_odt(cr, uid, ids, data, report_xml, context or {})
425
426     def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None):
427         return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {})
428
429     def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None):
430         return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {})
431
432     def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None):
433         if not context:
434             context={}
435         pool = pooler.get_pool(cr.dbname)
436         attach = report_xml.attachment
437         if attach:
438             objs = self.getObjects(cr, uid, ids, context)
439             results = []
440             for obj in objs:
441                 aname = eval(attach, {'object':obj, 'time':time})
442                 result = False
443                 if report_xml.attachment_use and aname and context.get('attachment_use', True):
444                     aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
445                     if aids:
446                         brow_rec = pool.get('ir.attachment').browse(cr, uid, aids[0])
447                         if not brow_rec.datas:
448                             continue
449                         d = base64.decodestring(brow_rec.datas)
450                         results.append((d,'pdf'))
451                         continue
452                 result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
453                 if not result:
454                     return False
455                 if aname:
456                     try:
457                         name = aname+'.'+result[1]
458                         pool.get('ir.attachment').create(cr, uid, {
459                             'name': aname,
460                             'datas': base64.encodestring(result[0]),
461                             'datas_fname': name,
462                             'res_model': self.table,
463                             'res_id': obj.id,
464                             }, context=context
465                         )
466                     except Exception:
467                         #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632
468                         logging.getLogger('report').error('Could not create saved report attachment', exc_info=True)
469                 results.append(result)
470             if results:
471                 if results[0][1]=='pdf':
472                     from pyPdf import PdfFileWriter, PdfFileReader
473                     output = PdfFileWriter()
474                     for r in results:
475                         reader = PdfFileReader(cStringIO.StringIO(r[0]))
476                         for page in range(reader.getNumPages()):
477                             output.addPage(reader.getPage(page))
478                     s = cStringIO.StringIO()
479                     output.write(s)
480                     return s.getvalue(), results[0][1]
481         return self.create_single_pdf(cr, uid, ids, data, report_xml, context)
482
483     def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
484         if not context:
485             context={}
486         logo = None
487         context = context.copy()
488         title = report_xml.name
489         rml = report_xml.report_rml_content
490         # if no rml file is found
491         if not rml:
492             return False
493         rml_parser = self.parser(cr, uid, self.name2, context=context)
494         objs = self.getObjects(cr, uid, ids, context)
495         rml_parser.set_context(objs, data, ids, report_xml.report_type)
496         processed_rml = etree.XML(rml)
497         if report_xml.header:
498             rml_parser._add_header(processed_rml, self.header)
499         processed_rml = self.preprocess_rml(processed_rml,report_xml.report_type)
500         if rml_parser.logo:
501             logo = base64.decodestring(rml_parser.logo)
502         create_doc = self.generators[report_xml.report_type]
503         pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8'))
504         return (pdf, report_xml.report_type)
505
506     def create_single_odt(self, cr, uid, ids, data, report_xml, context=None):
507         if not context:
508             context={}
509         context = context.copy()
510         report_type = report_xml.report_type
511         context['parents'] = sxw_parents
512
513         # if binary content was passed as unicode, we must
514         # re-encode it as a 8-bit string using the pass-through
515         # 'latin1' encoding, to restore the original byte values.
516         # See also osv.fields.sanitize_binary_value()
517         binary_report_content = report_xml.report_sxw_content.encode("latin1")
518
519         sxw_io = StringIO.StringIO(binary_report_content)
520         sxw_z = zipfile.ZipFile(sxw_io, mode='r')
521         rml = sxw_z.read('content.xml')
522         meta = sxw_z.read('meta.xml')
523         mime_type = sxw_z.read('mimetype')
524         if mime_type == 'application/vnd.sun.xml.writer':
525             mime_type = 'sxw'
526         else :
527             mime_type = 'odt'
528         sxw_z.close()
529
530         rml_parser = self.parser(cr, uid, self.name2, context=context)
531         rml_parser.parents = sxw_parents
532         rml_parser.tag = sxw_tag
533         objs = self.getObjects(cr, uid, ids, context)
534         rml_parser.set_context(objs, data, ids, mime_type)
535
536         rml_dom_meta = node = etree.XML(meta)
537         elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined")
538         for pe in elements:
539             if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"):
540                 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3":
541                     pe[0].text=data['id']
542                 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4":
543                     pe[0].text=data['model']
544         meta = etree.tostring(rml_dom_meta, encoding='utf-8',
545                               xml_declaration=True)
546
547         rml_dom =  etree.XML(rml)
548         elements = []
549         key1 = rml_parser.localcontext['name_space']["text"]+"p"
550         key2 = rml_parser.localcontext['name_space']["text"]+"drop-down"
551         for n in rml_dom.iterdescendants():
552             if n.tag == key1:
553                 elements.append(n)
554         if mime_type == 'odt':
555             for pe in elements:
556                 e = pe.findall(key2)
557                 for de in e:
558                     pp=de.getparent()
559                     if de.text or de.tail:
560                         pe.text = de.text or de.tail
561                     for cnd in de:
562                         if cnd.text or cnd.tail:
563                             if pe.text:
564                                 pe.text +=  cnd.text or cnd.tail
565                             else:
566                                 pe.text =  cnd.text or cnd.tail
567                             pp.remove(de)
568         else:
569             for pe in elements:
570                 e = pe.findall(key2)
571                 for de in e:
572                     pp = de.getparent()
573                     if de.text or de.tail:
574                         pe.text = de.text or de.tail
575                     for cnd in de:
576                         text = cnd.get("{http://openoffice.org/2000/text}value",False)
577                         if text:
578                             if pe.text and text.startswith('[['):
579                                 pe.text +=  text
580                             elif text.startswith('[['):
581                                 pe.text =  text
582                             if de.getparent():
583                                 pp.remove(de)
584
585         rml_dom = self.preprocess_rml(rml_dom, mime_type)
586         create_doc = self.generators[mime_type]
587         odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
588                              encoding='utf-8', xml_declaration=True)
589         sxw_z = zipfile.ZipFile(sxw_io, mode='a')
590         sxw_z.writestr('content.xml', odt)
591         sxw_z.writestr('meta.xml', meta)
592
593         if report_xml.header:
594             #Add corporate header/footer
595             rml_file = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type))
596             try:
597                 rml = rml_file.read()
598                 rml_parser = self.parser(cr, uid, self.name2, context=context)
599                 rml_parser.parents = sxw_parents
600                 rml_parser.tag = sxw_tag
601                 objs = self.getObjects(cr, uid, ids, context)
602                 rml_parser.set_context(objs, data, ids, report_xml.report_type)
603                 rml_dom = self.preprocess_rml(etree.XML(rml),report_type)
604                 create_doc = self.generators[report_type]
605                 odt = create_doc(rml_dom,rml_parser.localcontext)
606                 if report_xml.header:
607                     rml_parser._add_header(odt)
608                 odt = etree.tostring(odt, encoding='utf-8',
609                                      xml_declaration=True)
610                 sxw_z.writestr('styles.xml', odt)
611             finally:
612                 rml_file.close()
613         sxw_z.close()
614         final_op = sxw_io.getvalue()
615         sxw_io.close()
616         return (final_op, mime_type)
617
618     def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
619         if not context:
620             context = {}
621         context = context.copy()
622         report_type = 'html'
623         context['parents'] = html_parents
624
625         html = report_xml.report_rml_content
626         html_parser = self.parser(cr, uid, self.name2, context=context)
627         html_parser.parents = html_parents
628         html_parser.tag = sxw_tag
629         objs = self.getObjects(cr, uid, ids, context)
630         html_parser.set_context(objs, data, ids, report_type)
631
632         html_dom =  etree.HTML(html)
633         html_dom = self.preprocess_rml(html_dom,'html2html')
634
635         create_doc = self.generators['html2html']
636         html = etree.tostring(create_doc(html_dom, html_parser.localcontext))
637
638         return (html.replace('&amp;','&').replace('&lt;', '<').replace('&gt;', '>').replace('</br>',''), report_type)
639
640     def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None):
641         mako_html = report_xml.report_rml_content
642         html_parser = self.parser(cr, uid, self.name2, context)
643         objs = self.getObjects(cr, uid, ids, context)
644         html_parser.set_context(objs, data, ids, 'html')
645         create_doc = self.generators['makohtml2html']
646         html = create_doc(mako_html,html_parser.localcontext)
647         return (html,'html')
648