1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
21 from lxml import etree
25 from datetime import datetime
29 from interface import report_rml
32 import openerp.tools as tools
37 from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
38 from openerp.tools.translate import _
39 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
41 _logger = logging.getLogger(__name__)
70 def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
71 return len((datetime.now()).strftime(date_format))
73 class _format(object):
74 def set_value(self, cr, uid, name, object, field, lang_obj):
78 self.lang_obj = lang_obj
80 class _float_format(float, _format):
81 def __init__(self,value):
82 super(_float_format, self).__init__()
83 self.val = value or 0.0
87 if hasattr(self,'_field') and getattr(self._field, 'digits', None):
88 digits = self._field.digits[1]
89 if hasattr(self, 'lang_obj'):
90 return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True)
93 class _int_format(int, _format):
94 def __init__(self,value):
95 super(_int_format, self).__init__()
99 if hasattr(self,'lang_obj'):
100 return self.lang_obj.format('%.d', self.name, True)
103 class _date_format(str, _format):
104 def __init__(self,value):
105 super(_date_format, self).__init__()
106 self.val = value and str(value) or ''
110 if getattr(self,'name', None):
111 date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT)
112 return date.strftime(self.lang_obj.date_format.encode('utf-8'))
115 class _dttime_format(str, _format):
116 def __init__(self,value):
117 super(_dttime_format, self).__init__()
118 self.val = value and str(value) or ''
121 if self.val and getattr(self,'name', None):
122 return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\
123 .strftime("%s %s"%((self.lang_obj.date_format).encode('utf-8'),
124 (self.lang_obj.time_format).encode('utf-8')))
129 'float': _float_format,
130 'date': _date_format,
131 'integer': _int_format,
132 'datetime' : _dttime_format
136 # Context: {'node': node.dom}
138 class browse_record_list(list):
139 def __init__(self, lst, context):
140 super(browse_record_list, self).__init__(lst)
141 self.context = context
143 def __getattr__(self, name):
144 res = browse_record_list([getattr(x,name) for x in self], self.context)
148 return "browse_record_list("+str(len(self))+")"
150 class rml_parse(object):
151 def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None):
156 self.pool = openerp.registry(cr.dbname)
157 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
158 self.localcontext = {
160 'setCompany': self.setCompany,
161 'repeatIn': self.repeatIn,
162 'setLang': self.setLang,
163 'setTag': self.setTag,
164 'removeParentNode': self.removeParentNode,
165 'format': self.format,
166 'formatLang': self.formatLang,
167 'lang' : user.company_id.partner_id.lang,
168 'translate' : self._translate,
169 'setHtmlImage' : self.set_html_image,
170 'strip_name' : self._strip_name,
172 'display_address': self.display_address,
173 # more context members are setup in setCompany() below:
177 self.setCompany(user.company_id)
178 self.localcontext.update(context)
181 self.parents = parents
183 self._lang_cache = {}
185 self.default_lang = {}
186 self.lang_dict_called = False
187 self._transl_regex = re.compile('(\[\[.+?\]\])')
189 def setTag(self, oldtag, newtag, attrs=None):
192 def _ellipsis(self, char, size=100, truncation_str='...'):
195 if len(char) <= size:
197 return char[:size-len(truncation_str)] + truncation_str
199 def setCompany(self, company_id):
201 self.localcontext['company'] = company_id
202 self.localcontext['logo'] = company_id.logo
203 self.rml_header = company_id.rml_header
204 self.rml_header2 = company_id.rml_header2
205 self.rml_header3 = company_id.rml_header3
206 self.logo = company_id.logo
208 def _strip_name(self, name, maxlen=50):
209 return self._ellipsis(name, maxlen)
211 def format(self, text, oldtag=None):
214 def removeParentNode(self, tag=None):
215 raise GeneratorExit('Skip')
217 def set_html_image(self,id,model=None,field=None,context=None):
221 model = 'ir.attachment'
224 res = self.pool[model].read(self.cr,self.uid,id)
227 elif model =='ir.attachment' :
234 def setLang(self, lang):
235 self.localcontext['lang'] = lang
236 self.lang_dict_called = False
237 for obj in self.objects:
238 obj._context['lang'] = lang
240 def _get_lang_dict(self):
241 pool_lang = self.pool['res.lang']
242 lang = self.localcontext.get('lang', 'en_US') or 'en_US'
243 lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0]
244 lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids)
245 self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format})
246 self.default_lang[lang] = self.lang_dict.copy()
249 def digits_fmt(self, obj=None, f=None, dp=None):
250 digits = self.get_digits(obj, f, dp)
251 return "%%.%df" % (digits, )
253 def get_digits(self, obj=None, f=None, dp=None):
254 d = DEFAULT_DIGITS = 2
256 decimal_precision_obj = self.pool['decimal.precision']
257 ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)])
259 d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits
261 res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
262 if isinstance(res_digits, tuple):
265 d = res_digits(self.cr)[1]
266 elif (hasattr(obj, '_field') and\
267 isinstance(obj._field, (float_field, function_field)) and\
269 d = obj._field.digits[1] or DEFAULT_DIGITS
272 def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
274 Assuming 'Account' decimal.precision=3:
275 formatLang(value) -> digits=2 (default)
276 formatLang(value, digits=4) -> digits=4
277 formatLang(value, dp='Account') -> digits=3
278 formatLang(value, digits=5, dp='Account') -> digits=5
282 digits = self.get_digits(dp=dp)
284 digits = self.get_digits(value)
286 if isinstance(value, (str, unicode)) and not value:
289 if not self.lang_dict_called:
290 self._get_lang_dict()
291 self.lang_dict_called = True
293 if date or date_time:
297 date_format = self.lang_dict['date_format']
298 parse_format = DEFAULT_SERVER_DATE_FORMAT
300 value = value.split('.')[0]
301 date_format = date_format + " " + self.lang_dict['time_format']
302 parse_format = DEFAULT_SERVER_DATETIME_FORMAT
303 if isinstance(value, basestring):
304 # FIXME: the trimming is probably unreliable if format includes day/month names
305 # and those would need to be translated anyway.
306 date = datetime.strptime(value[:get_date_length(parse_format)], parse_format)
307 elif isinstance(value, time.struct_time):
308 date = datetime(*value[:6])
310 date = datetime(*value.timetuple()[:6])
312 # Convert datetime values to the expected client/context timezone
313 date = datetime_field.context_timestamp(self.cr, self.uid,
315 context=self.localcontext)
316 return date.strftime(date_format.encode('utf-8'))
318 res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
320 if currency_obj.position == 'after':
321 res='%s %s'%(res,currency_obj.symbol)
322 elif currency_obj and currency_obj.position == 'before':
323 res='%s %s'%(currency_obj.symbol, res)
326 def display_address(self, address_browse_record):
327 return self.pool['res.partner']._display_address(self.cr, self.uid, address_browse_record)
329 def repeatIn(self, lst, name,nodes_parent=False):
332 ret_lst.append({name:id})
335 def _translate(self,text):
336 lang = self.localcontext['lang']
337 if lang and text and not text.isspace():
338 transl_obj = self.pool['ir.translation']
339 piece_list = self._transl_regex.split(text)
340 for pn in range(len(piece_list)):
341 if not self._transl_regex.match(piece_list[pn]):
342 source_string = piece_list[pn].replace('\n', ' ').strip()
343 if len(source_string):
344 translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string)
345 if translated_string:
346 piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
347 text = ''.join(piece_list)
350 def _add_header(self, rml_dom, header='external'):
351 if header=='internal':
352 rml_head = self.rml_header2
353 elif header=='internal landscape':
354 rml_head = self.rml_header3
356 rml_head = self.rml_header
358 head_dom = etree.XML(rml_head)
360 found = rml_dom.find('.//'+tag.tag)
361 if found is not None and len(found):
362 if tag.get('position'):
365 found.getparent().replace(found,tag)
368 def set_context(self, objects, data, ids, report_type = None):
369 self.localcontext['data'] = data
370 self.localcontext['objects'] = objects
371 self.localcontext['digits_fmt'] = self.digits_fmt
372 self.localcontext['get_digits'] = self.get_digits
375 self.objects = objects
377 if report_type=='odt' :
378 self.localcontext.update({'name_space' :common.odt_namespace})
380 self.localcontext.update({'name_space' :common.sxw_namespace})
382 # WARNING: the object[0].exists() call below is slow but necessary because
383 # some broken reporting wizards pass incorrect IDs (e.g. ir.ui.menu ids)
384 if objects and len(objects) == 1 and \
385 objects[0].exists() and 'company_id' in objects[0] and objects[0].company_id:
386 # When we print only one record, we can auto-set the correct
387 # company in the localcontext. For other cases the report
388 # will have to call setCompany() inside the main repeatIn loop.
389 self.setCompany(objects[0].company_id)
391 class report_sxw(report_rml, preprocess.report):
393 The register=True kwarg has been added to help remove the
394 openerp.netsvc.LocalService() indirection and the related
395 openerp.report.interface.report_int._reports dictionary:
396 report_sxw registered in XML with auto=False are also registered in Python.
397 In that case, they are registered in the above dictionary. Since
398 registration is automatically done upon instanciation, and that
399 instanciation is needed before rendering, a way was needed to
400 instanciate-without-register a report. In the future, no report
401 should be registered in the above dictionary and it will be dropped.
403 def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False, register=True):
404 report_rml.__init__(self, name, table, rml, '', register=register)
409 self.internal_header=False
410 if header=='internal' or header=='internal landscape':
411 self.internal_header=True
413 def getObjects(self, cr, uid, ids, context):
414 table_obj = openerp.registry(cr.dbname)[self.table]
415 return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
417 def create(self, cr, uid, ids, data, context=None):
420 if self.internal_header:
421 context.update(internal_header=self.internal_header)
422 # skip osv.fields.sanitize_binary_value() because we want the raw bytes in all cases
423 context.update(bin_raw=True)
424 registry = openerp.registry(cr.dbname)
425 ir_obj = registry['ir.actions.report.xml']
426 report_xml_ids = ir_obj.search(cr, uid,
427 [('report_name', '=', self.name[7:])], context=context)
429 report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context)
432 report_file = tools.file_open(self.tmpl, subdir=None)
434 rml = report_file.read()
435 report_type= data.get('report_type', 'pdf')
437 def __init__(self, *args, **argv):
438 for key,arg in argv.items():
439 setattr(self, key, arg)
440 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
443 if report_xml.header:
444 report_xml.header = self.header
445 report_type = report_xml.report_type
446 if report_type in ['sxw','odt']:
447 fnct = self.create_source_odt
448 elif report_type in ['pdf','raw','txt','html']:
449 fnct = self.create_source_pdf
450 elif report_type=='html2html':
451 fnct = self.create_source_html2html
452 elif report_type=='mako2html':
453 fnct = self.create_source_mako2html
455 raise NotImplementedError(_('Unknown report type: %s') % report_type)
456 fnct_ret = fnct(cr, uid, ids, data, report_xml, context)
461 def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
462 return self.create_single_odt(cr, uid, ids, data, report_xml, context or {})
464 def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None):
465 return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {})
467 def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None):
468 return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {})
470 def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None):
473 registry = openerp.registry(cr.dbname)
474 attach = report_xml.attachment
476 objs = self.getObjects(cr, uid, ids, context)
479 aname = eval(attach, {'object':obj, 'time':time})
481 if report_xml.attachment_use and aname and context.get('attachment_use', True):
482 aids = registry['ir.attachment'].search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
484 brow_rec = registry['ir.attachment'].browse(cr, uid, aids[0])
485 if not brow_rec.datas:
487 d = base64.decodestring(brow_rec.datas)
488 results.append((d,'pdf'))
490 result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
495 name = aname+'.'+result[1]
496 # Remove the default_type entry from the context: this
497 # is for instance used on the account.account_invoices
498 # and is thus not intended for the ir.attachment type
501 ctx.pop('default_type', None)
502 registry['ir.attachment'].create(cr, uid, {
504 'datas': base64.encodestring(result[0]),
506 'res_model': self.table,
511 #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632
512 _logger.error('Could not create saved report attachment', exc_info=True)
513 results.append(result)
515 if results[0][1]=='pdf':
516 from pyPdf import PdfFileWriter, PdfFileReader
517 output = PdfFileWriter()
519 reader = PdfFileReader(cStringIO.StringIO(r[0]))
520 for page in range(reader.getNumPages()):
521 output.addPage(reader.getPage(page))
522 s = cStringIO.StringIO()
524 return s.getvalue(), results[0][1]
525 return self.create_single_pdf(cr, uid, ids, data, report_xml, context)
527 def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
531 context = context.copy()
532 title = report_xml.name
533 rml = report_xml.report_rml_content
534 # if no rml file is found
537 rml_parser = self.parser(cr, uid, self.name2, context=context)
538 objs = self.getObjects(cr, uid, ids, context)
539 rml_parser.set_context(objs, data, ids, report_xml.report_type)
540 processed_rml = etree.XML(rml)
541 if report_xml.header:
542 rml_parser._add_header(processed_rml, self.header)
543 processed_rml = self.preprocess_rml(processed_rml,report_xml.report_type)
545 logo = base64.decodestring(rml_parser.logo)
546 create_doc = self.generators[report_xml.report_type]
547 pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8'))
548 return pdf, report_xml.report_type
550 def create_single_odt(self, cr, uid, ids, data, report_xml, context=None):
553 context = context.copy()
554 report_type = report_xml.report_type
555 context['parents'] = sxw_parents
556 binary_report_content = report_xml.report_sxw_content
557 if isinstance(report_xml.report_sxw_content, unicode):
558 # if binary content was passed as unicode, we must
559 # re-encode it as a 8-bit string using the pass-through
560 # 'latin1' encoding, to restore the original byte values.
561 # See also osv.fields.sanitize_binary_value()
562 binary_report_content = report_xml.report_sxw_content.encode("latin1")
564 sxw_io = StringIO.StringIO(binary_report_content)
565 sxw_z = zipfile.ZipFile(sxw_io, mode='r')
566 rml = sxw_z.read('content.xml')
567 meta = sxw_z.read('meta.xml')
568 mime_type = sxw_z.read('mimetype')
569 if mime_type == 'application/vnd.sun.xml.writer':
575 rml_parser = self.parser(cr, uid, self.name2, context=context)
576 rml_parser.parents = sxw_parents
577 rml_parser.tag = sxw_tag
578 objs = self.getObjects(cr, uid, ids, context)
579 rml_parser.set_context(objs, data, ids, mime_type)
581 rml_dom_meta = node = etree.XML(meta)
582 elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined")
584 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"):
585 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3":
586 pe[0].text=data['id']
587 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4":
588 pe[0].text=data['model']
589 meta = etree.tostring(rml_dom_meta, encoding='utf-8',
590 xml_declaration=True)
592 rml_dom = etree.XML(rml)
594 key1 = rml_parser.localcontext['name_space']["text"]+"p"
595 key2 = rml_parser.localcontext['name_space']["text"]+"drop-down"
596 for n in rml_dom.iterdescendants():
599 if mime_type == 'odt':
604 if de.text or de.tail:
605 pe.text = de.text or de.tail
607 if cnd.text or cnd.tail:
609 pe.text += cnd.text or cnd.tail
611 pe.text = cnd.text or cnd.tail
618 if de.text or de.tail:
619 pe.text = de.text or de.tail
621 text = cnd.get("{http://openoffice.org/2000/text}value",False)
623 if pe.text and text.startswith('[['):
625 elif text.startswith('[['):
630 rml_dom = self.preprocess_rml(rml_dom, mime_type)
631 create_doc = self.generators[mime_type]
632 odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
633 encoding='utf-8', xml_declaration=True)
634 sxw_contents = {'content.xml':odt, 'meta.xml':meta}
636 if report_xml.header:
637 #Add corporate header/footer
638 rml_file = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type))
640 rml = rml_file.read()
641 rml_parser = self.parser(cr, uid, self.name2, context=context)
642 rml_parser.parents = sxw_parents
643 rml_parser.tag = sxw_tag
644 objs = self.getObjects(cr, uid, ids, context)
645 rml_parser.set_context(objs, data, ids, report_xml.report_type)
646 rml_dom = self.preprocess_rml(etree.XML(rml),report_type)
647 create_doc = self.generators[report_type]
648 odt = create_doc(rml_dom,rml_parser.localcontext)
649 if report_xml.header:
650 rml_parser._add_header(odt)
651 odt = etree.tostring(odt, encoding='utf-8',
652 xml_declaration=True)
653 sxw_contents['styles.xml'] = odt
657 #created empty zip writing sxw contents to avoid duplication
658 sxw_out = StringIO.StringIO()
659 sxw_out_zip = zipfile.ZipFile(sxw_out, mode='w')
660 sxw_template_zip = zipfile.ZipFile (sxw_io, 'r')
661 for item in sxw_template_zip.infolist():
662 if item.filename not in sxw_contents:
663 buffer = sxw_template_zip.read(item.filename)
664 sxw_out_zip.writestr(item.filename, buffer)
665 for item_filename, buffer in sxw_contents.iteritems():
666 sxw_out_zip.writestr(item_filename, buffer)
667 sxw_template_zip.close()
669 final_op = sxw_out.getvalue()
672 return final_op, mime_type
674 def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
677 context = context.copy()
679 context['parents'] = html_parents
681 html = report_xml.report_rml_content
682 html_parser = self.parser(cr, uid, self.name2, context=context)
683 html_parser.parents = html_parents
684 html_parser.tag = sxw_tag
685 objs = self.getObjects(cr, uid, ids, context)
686 html_parser.set_context(objs, data, ids, report_type)
688 html_dom = etree.HTML(html)
689 html_dom = self.preprocess_rml(html_dom,'html2html')
691 create_doc = self.generators['html2html']
692 html = etree.tostring(create_doc(html_dom, html_parser.localcontext))
694 return html.replace('&','&').replace('<', '<').replace('>', '>').replace('</br>',''), report_type
696 def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None):
697 mako_html = report_xml.report_rml_content
698 html_parser = self.parser(cr, uid, self.name2, context)
699 objs = self.getObjects(cr, uid, ids, context)
700 html_parser.set_context(objs, data, ids, 'html')
701 create_doc = self.generators['makohtml2html']
702 html = create_doc(mako_html,html_parser.localcontext)
706 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: