[IMP] Added YAML for demo data.
[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 traceback, sys
23 import StringIO
24 import cStringIO
25 import base64
26 import copy
27 import locale
28 from datetime import datetime
29 import os
30 import re
31 import time
32 from interface import report_rml
33 import preprocess
34 import ir
35 import netsvc
36 import osv
37 import pooler
38 import tools
39 import warnings
40 import zipfile
41 import common
42
43 DT_FORMAT = '%Y-%m-%d'
44 DHM_FORMAT = '%Y-%m-%d %H:%M:%S'
45 HM_FORMAT = '%H:%M:%S'
46
47 rml_parents = {
48     'tr':1,
49     'li':1,
50     'story': 0,
51     'section': 0
52 }
53
54 rml_tag="para"
55
56 sxw_parents = {
57     'table-row': 1,
58     'list-item': 1,
59     'body': 0,
60     'section': 0,
61 }
62
63 html_parents = {
64     'tr' : 1,
65     'body' : 0,
66     'div' : 0
67     }
68 sxw_tag = "p"
69
70 rml2sxw = {
71     'para': 'p',
72 }
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
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 self.val
93
94 class _int_format(int, _format):
95     def __init__(self,value):
96         super(_int_format, self).__init__()
97         self.val = value and str(value) or str(0)
98
99     def __str__(self):
100         if hasattr(self,'lang_obj'):
101             return self.lang_obj.format('%.d', self.name, True)
102         return 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, DT_FORMAT)
113                 return date.strftime(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"%(self.lang_obj.date_format,
125                                       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             'company': user.company_id,
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             'logo' : user.company_id.logo,
169             'lang' : user.company_id.partner_id.lang,
170             'translate' : self._translate,
171             'setHtmlImage' : self.set_html_image,
172             'time' : time
173         }
174         self.localcontext.update(context)
175         self.rml_header = user.company_id.rml_header
176         self.rml_header2 = user.company_id.rml_header2
177         self.logo = user.company_id.logo
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 format(self, text, oldtag=None):
192         return text.strip()
193
194     def removeParentNode(self, tag=None):
195         raise Exception('Skip')
196
197     def set_html_image(self,id,model=None,field=None,context=None):
198         if not id :
199             return ''
200         if not model:
201             model = 'ir.attachment'
202         try :
203             id = int(id)
204             res = self.pool.get(model).read(self.cr,self.uid,id)
205             if field :
206                 return res[field]
207             elif model =='ir.attachment' :
208                 return res['datas']
209             else :
210                 return ''
211         except Exception,e:
212             return ''
213
214     def setLang(self, lang):
215         if not lang or self.default_lang.has_key(lang):
216             if not lang:
217                 key = 'en_US'
218         self.localcontext['lang'] = lang
219         self.lang_dict_called = False
220         for obj in self.objects:
221             obj._context['lang'] = lang
222             for table in obj._cache:
223                 for id in obj._cache[table]:
224                     self._lang_cache.setdefault(obj._context['lang'], {}).setdefault(table,
225                             {}).update(obj._cache[table][id])
226                     if lang in self._lang_cache \
227                             and table in self._lang_cache[lang] \
228                             and id in self._lang_cache[lang][table]:
229                         obj._cache[table][id] = self._lang_cache[lang][table][id]
230                     else:
231                         obj._cache[table][id] = {'id': id}
232
233     def _get_lang_dict(self):
234         pool_lang = self.pool.get('res.lang')
235         lang = self.localcontext.get('lang', 'en_US') or 'en_US'
236         lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0]
237         lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids)
238         self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format})
239         self.default_lang[lang] = self.lang_dict.copy()
240         return True
241
242     def formatLang(self, value, digits=2, date=False,date_time=False, grouping=True, monetary=False):
243         if isinstance(value, (str, unicode)) and not value:
244             return ''
245         if not self.lang_dict_called:
246             self._get_lang_dict()
247             self.lang_dict_called = True
248
249         if date or date_time:
250             if not str(value):
251                 return ''
252             date_format = self.lang_dict['date_format']
253             parse_format = DT_FORMAT
254             if date_time:
255                 date_format = date_format + " " + self.lang_dict['time_format']
256                 parse_format = DHM_FORMAT
257
258             # filtering time.strftime('%Y-%m-%d')
259 #            if type(value) == type(''):
260 #                parse_format = DHM_FORMAT
261 #                if (not date_time):
262 #                    return str(value)
263
264             if not isinstance(value, time.struct_time):
265                 try:
266                     date = datetime.datetime(str(value), parse_format)
267                 except:# sometimes it takes converted values into value, so we dont need conversion.
268                     return str(value)
269             else:
270                 date = datetime(*value.timetuple()[:6])
271             return date.strftime(date_format)
272         return self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
273
274     def repeatIn(self, lst, name,nodes_parent=False):
275         ret_lst = []
276         for id in lst:
277             ret_lst.append({name:id})
278         return ret_lst
279
280     def _translate(self,text):
281         lang = self.localcontext['lang']
282         if lang and text and not text.isspace():
283             transl_obj = self.pool.get('ir.translation')
284             piece_list = self._transl_regex.split(text)
285             for pn in range(len(piece_list)):
286                 if not self._transl_regex.match(piece_list[pn]):
287                     source_string = piece_list[pn].replace('\n', ' ').strip()
288                     if len(source_string):
289                         translated_string = transl_obj._get_source(self.cr, self.uid, self.name, 'rml', lang, source_string)
290                         if translated_string:
291                             piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
292             text = ''.join(piece_list)
293         return text
294
295     def _add_header(self, rml_dom, header=1):
296         if header==2:
297             rml_head =  self.rml_header2
298         else:
299             rml_head =  self.rml_header
300         if self.logo and (rml_head.find('company.logo')<0 or rml_head.find('<image')<0) and rml_head.find('<!--image')<0:
301             rml_head =  rml_head.replace('<pageGraphics>','''<pageGraphics> <image x="10" y="26cm" height="70" width="90" >[[company.logo]] </image> ''')
302         if not self.logo and rml_head.find('company.logo')>=0:
303             rml_head = rml_head.replace('<image','<!--image')
304             rml_head = rml_head.replace('</image>','</image-->')
305         head_dom = etree.XML(rml_head)
306         for tag in head_dom:
307             found = rml_dom.find('.//'+tag.tag)
308             if found is not None and len(found):
309                 if tag.get('position'):
310                     found.append(tag)
311                 else :
312                     found.getparent().replace(found,tag)
313         return True
314
315     def set_context(self, objects, data, ids, report_type = None):
316         self.localcontext['data'] = data
317         self.localcontext['objects'] = objects
318         self.datas = data
319         self.ids = ids
320         self.objects = objects
321         if report_type:
322             if report_type=='odt' :
323                 self.localcontext.update({'name_space' :common.odt_namespace})
324             else:
325                 self.localcontext.update({'name_space' :common.sxw_namespace})
326
327 class report_sxw(report_rml, preprocess.report):
328     def __init__(self, name, table, rml=False, parser=rml_parse, header=True, store=False):
329         report_rml.__init__(self, name, table, rml, '')
330         self.name = name
331         self.parser = parser
332         self.header = header
333         self.store = store
334
335     def getObjects(self, cr, uid, ids, context):
336         table_obj = pooler.get_pool(cr.dbname).get(self.table)
337         return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
338
339     def create(self, cr, uid, ids, data, context=None):
340         pool = pooler.get_pool(cr.dbname)
341         ir_obj = pool.get('ir.actions.report.xml')
342         report_xml_ids = ir_obj.search(cr, uid,
343                 [('report_name', '=', self.name[7:])], context=context)
344         if report_xml_ids:
345             report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context)
346         else:
347             title = ''
348             rml = tools.file_open(self.tmpl, subdir=None).read()
349             report_type= data.get('report_type', 'pdf')
350             class a(object):
351                 def __init__(self, *args, **argv):
352                     for key,arg in argv.items():
353                         setattr(self, key, arg)
354             report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
355         report_type = report_xml.report_type
356         if report_type in ['sxw','odt']:
357             fnct = self.create_source_odt
358         elif report_type in ['pdf','raw','txt','html']:
359             fnct = self.create_source_pdf
360         elif report_type=='html2html':
361             fnct = self.create_source_html2html
362         elif report_type=='mako2html':
363             fnct = self.create_source_mako2html
364         else:
365             raise 'Unknown Report Type'
366         fnct_ret = fnct(cr, uid, ids, data, report_xml, context)
367         if not fnct_ret:
368             return (False,False)
369         return fnct_ret
370
371     def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
372         return self.create_single_odt(cr, uid, ids, data, report_xml, context or {})
373
374     def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None):
375         return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {})
376
377     def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None):
378         return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {})
379
380     def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None):
381         if not context:
382             context={}
383         pool = pooler.get_pool(cr.dbname)
384         attach = report_xml.attachment
385         if attach:
386             objs = self.getObjects(cr, uid, ids, context)
387             results = []
388             for obj in objs:
389                 aname = eval(attach, {'object':obj, 'time':time})
390                 result = False
391                 if report_xml.attachment_use and aname and context.get('attachment_use', True):
392                     aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
393                     if aids:
394                         brow_rec = pool.get('ir.attachment').browse(cr, uid, aids[0])
395                         if not brow_rec.datas:
396                             continue
397                         d = base64.decodestring(brow_rec.datas)
398                         results.append((d,'pdf'))
399                         continue
400                 result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
401                 if not result:
402                     return False
403                 try:
404                     if aname:
405                         name = aname+'.'+result[1]
406                         pool.get('ir.attachment').create(cr, uid, {
407                             'name': aname,
408                             'datas': base64.encodestring(result[0]),
409                             'datas_fname': name,
410                             'res_model': self.table,
411                             'res_id': obj.id,
412                             }, context=context
413                         )
414                         cr.commit()
415                 except Exception,e:
416                      import traceback, sys
417                      tb_s = reduce(lambda x, y: x+y, traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
418                      netsvc.Logger().notifyChannel('report', netsvc.LOG_ERROR,str(e))
419                 results.append(result)
420             if results:
421                 if results[0][1]=='pdf':
422                     from pyPdf import PdfFileWriter, PdfFileReader
423                     output = PdfFileWriter()
424                     for r in results:
425                         reader = PdfFileReader(cStringIO.StringIO(r[0]))
426                         for page in range(reader.getNumPages()):
427                             output.addPage(reader.getPage(page))
428                     s = cStringIO.StringIO()
429                     output.write(s)
430                     return s.getvalue(), results[0][1]
431         return self.create_single_pdf(cr, uid, ids, data, report_xml, context)
432
433     def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
434         if not context:
435             context={}
436         logo = None
437         context = context.copy()
438         title = report_xml.name
439         rml = report_xml.report_rml_content
440         # if no rml file is found
441         if not rml:
442             return False
443         rml_parser = self.parser(cr, uid, self.name2, context=context)
444         objs = self.getObjects(cr, uid, ids, context)
445         rml_parser.set_context(objs, data, ids, report_xml.report_type)
446         processed_rml = self.preprocess_rml(etree.XML(rml),report_xml.report_type)
447         if report_xml.header:
448             rml_parser._add_header(processed_rml)
449         if rml_parser.logo:
450             logo = base64.decodestring(rml_parser.logo)
451         create_doc = self.generators[report_xml.report_type]
452         pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8'))
453         return (pdf, report_xml.report_type)
454
455     def create_single_odt(self, cr, uid, ids, data, report_xml, context=None):
456         if not context:
457             context={}
458         context = context.copy()
459         report_type = report_xml.report_type
460         context['parents'] = sxw_parents
461         sxw_io = StringIO.StringIO(report_xml.report_sxw_content)
462         sxw_z = zipfile.ZipFile(sxw_io, mode='r')
463         rml = sxw_z.read('content.xml')
464         meta = sxw_z.read('meta.xml')
465         sxw_z.close()
466
467         rml_parser = self.parser(cr, uid, self.name2, context=context)
468         rml_parser.parents = sxw_parents
469         rml_parser.tag = sxw_tag
470         objs = self.getObjects(cr, uid, ids, context)
471         rml_parser.set_context(objs, data, ids,report_xml.report_type)
472
473         rml_dom_meta = node = etree.XML(meta)
474         elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined")
475         for pe in elements:
476             if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"):
477                 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3":
478                     pe[0].text=data['id']
479                 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4":
480                     pe[0].text=data['model']
481         meta = etree.tostring(rml_dom_meta, encoding='utf-8',
482                               xml_declaration=True)
483
484         rml_dom =  etree.XML(rml)
485         body = rml_dom[-1]
486         elements = []
487         key1 = rml_parser.localcontext['name_space']["text"]+"p"
488         key2 = rml_parser.localcontext['name_space']["text"]+"drop-down"
489         for n in rml_dom.iterdescendants():
490             if n.tag == key1:
491                 elements.append(n)
492         if report_type == 'odt':
493             for pe in elements:
494                 e = pe.findall(key2)
495                 for de in e:
496                     pp=de.getparent()
497                     if de.text or de.tail:
498                         pe.text = de.text or de.tail
499                     for cnd in de:
500                         if cnd.text or cnd.tail:
501                             if pe.text:
502                                 pe.text +=  cnd.text or cnd.tail
503                             else:
504                                 pe.text =  cnd.text or cnd.tail
505                             pp.remove(de)
506         else:
507             for pe in elements:
508                 e = pe.findall(key2)
509                 for de in e:
510                     pp = de.getparent()
511                     if de.text or de.tail:
512                         pe.text = de.text or de.tail
513                     for cnd in de:
514                         text = cnd.get("{http://openoffice.org/2000/text}value",False)
515                         if text:
516                             if pe.text and text.startswith('[['):
517                                 pe.text +=  text
518                             elif text.startswith('[['):
519                                 pe.text =  text
520                             if de.getparent():
521                                 pp.remove(de)
522
523         rml_dom = self.preprocess_rml(rml_dom,report_type)
524         create_doc = self.generators[report_type]
525         odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
526                              encoding='utf-8', xml_declaration=True)
527         sxw_z = zipfile.ZipFile(sxw_io, mode='a')
528         sxw_z.writestr('content.xml', odt)
529         sxw_z.writestr('meta.xml', meta)
530
531         if report_xml.header:
532             #Add corporate header/footer
533             rml = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type)).read()
534             rml_parser = self.parser(cr, uid, self.name2, context=context)
535             rml_parser.parents = sxw_parents
536             rml_parser.tag = sxw_tag
537             objs = self.getObjects(cr, uid, ids, context)
538             rml_parser.set_context(objs, data, ids, report_xml.report_type)
539             rml_dom = self.preprocess_rml(etree.XML(rml),report_type)
540             create_doc = self.generators[report_type]
541             odt = create_doc(rml_dom,rml_parser.localcontext)
542             if report_xml.header:
543                 rml_parser._add_header(odt)
544             odt = etree.tostring(odt, encoding='utf-8',
545                                  xml_declaration=True)
546             sxw_z.writestr('styles.xml', odt)
547         sxw_z.close()
548         final_op = sxw_io.getvalue()
549         sxw_io.close()
550         return (final_op, report_type)
551
552     def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
553         if not context:
554             context = {}
555         context = context.copy()
556         report_type = 'html'
557         context['parents'] = html_parents
558
559         html = report_xml.report_rml_content
560         html_parser = self.parser(cr, uid, self.name2, context=context)
561         html_parser.parents = html_parents
562         html_parser.tag = sxw_tag
563         objs = self.getObjects(cr, uid, ids, context)
564         html_parser.set_context(objs, data, ids, report_type)
565
566         html_dom =  etree.HTML(html)
567         html_dom = self.preprocess_rml(html_dom,'html2html')
568
569         create_doc = self.generators['html2html']
570         html = etree.tostring(create_doc(html_dom, html_parser.localcontext))
571
572         return (html.replace('&amp;','&').replace('&lt;', '<').replace('&gt;', '>').replace('</br>',''), report_type)
573
574     def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None):
575         mako_html = report_xml.report_rml_content
576         html_parser = self.parser(cr, uid, self.name2, context)
577         objs = self.getObjects(cr, uid, ids, context)
578         html_parser.set_context(objs, data, ids, 'html')
579         create_doc = self.generators['makohtml2html']
580         html = create_doc(mako_html,html_parser.localcontext)
581         return (html,'html')
582