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