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.pooler as pooler
33 import openerp.tools as tools
36 from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
37 from openerp.tools.translate import _
38 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
40 _logger = logging.getLogger(__name__)
69 def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
70 return len((datetime.now()).strftime(date_format))
72 class _format(object):
73 def set_value(self, cr, uid, name, object, field, lang_obj):
77 self.lang_obj = lang_obj
79 class _float_format(float, _format):
80 def __init__(self,value):
81 super(_float_format, self).__init__()
82 self.val = value or 0.0
86 if hasattr(self,'_field') and getattr(self._field, 'digits', None):
87 digits = self._field.digits[1]
88 if hasattr(self, 'lang_obj'):
89 return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True)
92 class _int_format(int, _format):
93 def __init__(self,value):
94 super(_int_format, self).__init__()
98 if hasattr(self,'lang_obj'):
99 return self.lang_obj.format('%.d', self.name, True)
102 class _date_format(str, _format):
103 def __init__(self,value):
104 super(_date_format, self).__init__()
105 self.val = value and str(value) or ''
109 if getattr(self,'name', None):
110 date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT)
111 return date.strftime(self.lang_obj.date_format.encode('utf-8'))
114 class _dttime_format(str, _format):
115 def __init__(self,value):
116 super(_dttime_format, self).__init__()
117 self.val = value and str(value) or ''
120 if self.val and getattr(self,'name', None):
121 return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\
122 .strftime("%s %s"%((self.lang_obj.date_format).encode('utf-8'),
123 (self.lang_obj.time_format).encode('utf-8')))
128 'float': _float_format,
129 'date': _date_format,
130 'integer': _int_format,
131 'datetime' : _dttime_format
135 # Context: {'node': node.dom}
137 class browse_record_list(list):
138 def __init__(self, lst, context):
139 super(browse_record_list, self).__init__(lst)
140 self.context = context
142 def __getattr__(self, name):
143 res = browse_record_list([getattr(x,name) for x in self], self.context)
147 return "browse_record_list("+str(len(self))+")"
149 class rml_parse(object):
150 def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None):
155 self.pool = pooler.get_pool(cr.dbname)
156 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
157 self.localcontext = {
159 'setCompany': self.setCompany,
160 'repeatIn': self.repeatIn,
161 'setLang': self.setLang,
162 'setTag': self.setTag,
163 'removeParentNode': self.removeParentNode,
164 'format': self.format,
165 'formatLang': self.formatLang,
166 'lang' : user.company_id.partner_id.lang,
167 'translate' : self._translate,
168 'setHtmlImage' : self.set_html_image,
169 'strip_name' : self._strip_name,
171 'display_address': self.display_address,
172 # more context members are setup in setCompany() below:
176 self.setCompany(user.company_id)
177 self.localcontext.update(context)
180 self.parents = parents
182 self._lang_cache = {}
184 self.default_lang = {}
185 self.lang_dict_called = False
186 self._transl_regex = re.compile('(\[\[.+?\]\])')
188 def setTag(self, oldtag, newtag, attrs=None):
191 def _ellipsis(self, char, size=100, truncation_str='...'):
194 if len(char) <= size:
196 return char[:size-len(truncation_str)] + truncation_str
198 def setCompany(self, company_id):
200 self.localcontext['company'] = company_id
201 self.localcontext['logo'] = company_id.logo
202 self.rml_header = company_id.rml_header
203 self.rml_header2 = company_id.rml_header2
204 self.rml_header3 = company_id.rml_header3
205 self.logo = company_id.logo
207 def _strip_name(self, name, maxlen=50):
208 return self._ellipsis(name, maxlen)
210 def format(self, text, oldtag=None):
213 def removeParentNode(self, tag=None):
214 raise GeneratorExit('Skip')
216 def set_html_image(self,id,model=None,field=None,context=None):
220 model = 'ir.attachment'
223 res = self.pool.get(model).read(self.cr,self.uid,id)
226 elif model =='ir.attachment' :
233 def setLang(self, lang):
234 self.localcontext['lang'] = lang
235 self.lang_dict_called = False
236 for obj in self.objects:
237 obj._context['lang'] = lang
239 def _get_lang_dict(self):
240 pool_lang = self.pool.get('res.lang')
241 lang = self.localcontext.get('lang', 'en_US') or 'en_US'
242 lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])
244 lang_ids = pool_lang.search(self.cr,self.uid,[('code','=','en_US')])
245 lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids[0])
246 self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format})
247 self.default_lang[lang] = self.lang_dict.copy()
250 def digits_fmt(self, obj=None, f=None, dp=None):
251 digits = self.get_digits(obj, f, dp)
252 return "%%.%df" % (digits, )
254 def get_digits(self, obj=None, f=None, dp=None):
255 d = DEFAULT_DIGITS = 2
257 decimal_precision_obj = self.pool.get('decimal.precision')
258 d = decimal_precision_obj.precision_get(self.cr, self.uid, dp)
260 res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
261 if isinstance(res_digits, tuple):
264 d = res_digits(self.cr)[1]
265 elif (hasattr(obj, '_field') and\
266 isinstance(obj._field, (float_field, function_field)) and\
268 d = obj._field.digits[1]
269 if not d and d is not 0:
273 def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
275 Assuming 'Account' decimal.precision=3:
276 formatLang(value) -> digits=2 (default)
277 formatLang(value, digits=4) -> digits=4
278 formatLang(value, dp='Account') -> digits=3
279 formatLang(value, digits=5, dp='Account') -> digits=5
283 digits = self.get_digits(dp=dp)
285 digits = self.get_digits(value)
287 if isinstance(value, (str, unicode)) and not value:
290 if not self.lang_dict_called:
291 self._get_lang_dict()
292 self.lang_dict_called = True
294 if date or date_time:
298 date_format = self.lang_dict['date_format']
299 parse_format = DEFAULT_SERVER_DATE_FORMAT
301 value = value.split('.')[0]
302 date_format = date_format + " " + self.lang_dict['time_format']
303 parse_format = DEFAULT_SERVER_DATETIME_FORMAT
304 if isinstance(value, basestring):
305 # FIXME: the trimming is probably unreliable if format includes day/month names
306 # and those would need to be translated anyway.
307 date = datetime.strptime(value[:get_date_length(parse_format)], parse_format)
308 elif isinstance(value, time.struct_time):
309 date = datetime(*value[:6])
311 date = datetime(*value.timetuple()[:6])
313 # Convert datetime values to the expected client/context timezone
314 date = datetime_field.context_timestamp(self.cr, self.uid,
316 context=self.localcontext)
317 return date.strftime(date_format.encode('utf-8'))
319 res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
321 if currency_obj.position == 'after':
322 res='%s %s'%(res,currency_obj.symbol)
323 elif currency_obj and currency_obj.position == 'before':
324 res='%s %s'%(currency_obj.symbol, res)
327 def display_address(self, address_browse_record, without_company=False):
328 return self.pool.get('res.partner')._display_address(
329 self.cr, self.uid, address_browse_record,
330 without_company=without_company
333 def repeatIn(self, lst, name,nodes_parent=False):
336 ret_lst.append({name:id})
339 def _translate(self,text):
340 lang = self.localcontext['lang']
341 if lang and text and not text.isspace():
342 transl_obj = self.pool.get('ir.translation')
343 piece_list = self._transl_regex.split(text)
344 for pn in range(len(piece_list)):
345 if not self._transl_regex.match(piece_list[pn]):
346 source_string = piece_list[pn].replace('\n', ' ').strip()
347 if len(source_string):
348 translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string)
349 if translated_string:
350 piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
351 text = ''.join(piece_list)
354 def _add_header(self, rml_dom, header='external'):
355 if header=='internal':
356 rml_head = self.rml_header2
357 elif header=='internal landscape':
358 rml_head = self.rml_header3
360 rml_head = self.rml_header
362 head_dom = etree.XML(rml_head)
364 found = rml_dom.find('.//'+tag.tag)
365 if found is not None and len(found):
366 if tag.get('position'):
369 found.getparent().replace(found,tag)
372 def set_context(self, objects, data, ids, report_type = None):
373 self.localcontext['data'] = data
374 self.localcontext['objects'] = objects
375 self.localcontext['digits_fmt'] = self.digits_fmt
376 self.localcontext['get_digits'] = self.get_digits
379 self.objects = objects
381 if report_type=='odt' :
382 self.localcontext.update({'name_space' :common.odt_namespace})
384 self.localcontext.update({'name_space' :common.sxw_namespace})
386 # WARNING: the object[0].exists() call below is slow but necessary because
387 # some broken reporting wizards pass incorrect IDs (e.g. ir.ui.menu ids)
388 if objects and len(objects) == 1 and \
389 objects[0].exists() and 'company_id' in objects[0] and objects[0].company_id:
390 # When we print only one record, we can auto-set the correct
391 # company in the localcontext. For other cases the report
392 # will have to call setCompany() inside the main repeatIn loop.
393 self.setCompany(objects[0].company_id)
395 class report_sxw(report_rml, preprocess.report):
396 def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False):
397 report_rml.__init__(self, name, table, rml, '')
402 self.internal_header=False
403 if header=='internal' or header=='internal landscape':
404 self.internal_header=True
406 def getObjects(self, cr, uid, ids, context):
407 table_obj = pooler.get_pool(cr.dbname).get(self.table)
408 return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
410 def create(self, cr, uid, ids, data, context=None):
413 if self.internal_header:
414 context.update(internal_header=self.internal_header)
415 # skip osv.fields.sanitize_binary_value() because we want the raw bytes in all cases
416 context.update(bin_raw=True)
417 pool = pooler.get_pool(cr.dbname)
418 ir_obj = pool.get('ir.actions.report.xml')
419 report_xml_ids = ir_obj.search(cr, uid,
420 [('report_name', '=', self.name[7:])], context=context)
422 report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context)
425 report_file = tools.file_open(self.tmpl, subdir=None)
427 rml = report_file.read()
428 report_type= data.get('report_type', 'pdf')
430 def __init__(self, *args, **argv):
431 for key,arg in argv.items():
432 setattr(self, key, arg)
433 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
436 if report_xml.header:
437 report_xml.header = self.header
438 report_type = report_xml.report_type
439 if report_type in ['sxw','odt']:
440 fnct = self.create_source_odt
441 elif report_type in ['pdf','raw','txt','html']:
442 fnct = self.create_source_pdf
443 elif report_type=='html2html':
444 fnct = self.create_source_html2html
445 elif report_type=='mako2html':
446 fnct = self.create_source_mako2html
448 raise NotImplementedError(_('Unknown report type: %s') % report_type)
449 fnct_ret = fnct(cr, uid, ids, data, report_xml, context)
454 def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
455 return self.create_single_odt(cr, uid, ids, data, report_xml, context or {})
457 def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None):
458 return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {})
460 def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None):
461 return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {})
463 def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None):
466 pool = pooler.get_pool(cr.dbname)
467 attach = report_xml.attachment
469 objs = self.getObjects(cr, uid, ids, context)
472 aname = eval(attach, {'object':obj, 'time':time})
474 if report_xml.attachment_use and aname and context.get('attachment_use', True):
475 aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
477 brow_rec = pool.get('ir.attachment').browse(cr, uid, aids[0])
478 if not brow_rec.datas:
480 d = base64.decodestring(brow_rec.datas)
481 results.append((d,'pdf'))
483 result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
488 name = aname+'.'+result[1]
489 # Remove the default_type entry from the context: this
490 # is for instance used on the account.account_invoices
491 # and is thus not intended for the ir.attachment type
494 ctx.pop('default_type', None)
495 pool.get('ir.attachment').create(cr, uid, {
497 'datas': base64.encodestring(result[0]),
499 'res_model': self.table,
504 #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632
505 _logger.error('Could not create saved report attachment', exc_info=True)
506 results.append(result)
508 if results[0][1]=='pdf':
509 from pyPdf import PdfFileWriter, PdfFileReader
510 output = PdfFileWriter()
512 reader = PdfFileReader(cStringIO.StringIO(r[0]))
513 for page in range(reader.getNumPages()):
514 output.addPage(reader.getPage(page))
515 s = cStringIO.StringIO()
517 return s.getvalue(), results[0][1]
518 return self.create_single_pdf(cr, uid, ids, data, report_xml, context)
520 def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
524 context = context.copy()
525 title = report_xml.name
526 rml = report_xml.report_rml_content
527 # if no rml file is found
530 rml_parser = self.parser(cr, uid, self.name2, context=context)
531 objs = self.getObjects(cr, uid, ids, context)
532 rml_parser.set_context(objs, data, ids, report_xml.report_type)
533 processed_rml = etree.XML(rml)
534 if report_xml.header:
535 rml_parser._add_header(processed_rml, self.header)
536 processed_rml = self.preprocess_rml(processed_rml,report_xml.report_type)
538 logo = base64.decodestring(rml_parser.logo)
539 create_doc = self.generators[report_xml.report_type]
540 pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8'))
541 return pdf, report_xml.report_type
543 def create_single_odt(self, cr, uid, ids, data, report_xml, context=None):
546 context = context.copy()
547 report_type = report_xml.report_type
548 context['parents'] = sxw_parents
549 binary_report_content = report_xml.report_sxw_content
550 if isinstance(report_xml.report_sxw_content, unicode):
551 # if binary content was passed as unicode, we must
552 # re-encode it as a 8-bit string using the pass-through
553 # 'latin1' encoding, to restore the original byte values.
554 # See also osv.fields.sanitize_binary_value()
555 binary_report_content = report_xml.report_sxw_content.encode("latin1")
557 sxw_io = StringIO.StringIO(binary_report_content)
558 sxw_z = zipfile.ZipFile(sxw_io, mode='r')
559 rml = sxw_z.read('content.xml')
560 meta = sxw_z.read('meta.xml')
561 mime_type = sxw_z.read('mimetype')
562 if mime_type == 'application/vnd.sun.xml.writer':
568 rml_parser = self.parser(cr, uid, self.name2, context=context)
569 rml_parser.parents = sxw_parents
570 rml_parser.tag = sxw_tag
571 objs = self.getObjects(cr, uid, ids, context)
572 rml_parser.set_context(objs, data, ids, mime_type)
574 rml_dom_meta = node = etree.XML(meta)
575 elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined")
577 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"):
578 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3":
579 pe[0].text=data['id']
580 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4":
581 pe[0].text=data['model']
582 meta = etree.tostring(rml_dom_meta, encoding='utf-8',
583 xml_declaration=True)
585 rml_dom = etree.XML(rml)
587 key1 = rml_parser.localcontext['name_space']["text"]+"p"
588 key2 = rml_parser.localcontext['name_space']["text"]+"drop-down"
589 for n in rml_dom.iterdescendants():
592 if mime_type == 'odt':
597 if de.text or de.tail:
598 pe.text = de.text or de.tail
600 if cnd.text or cnd.tail:
602 pe.text += cnd.text or cnd.tail
604 pe.text = cnd.text or cnd.tail
611 if de.text or de.tail:
612 pe.text = de.text or de.tail
614 text = cnd.get("{http://openoffice.org/2000/text}value",False)
616 if pe.text and text.startswith('[['):
618 elif text.startswith('[['):
623 rml_dom = self.preprocess_rml(rml_dom, mime_type)
624 create_doc = self.generators[mime_type]
625 odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
626 encoding='utf-8', xml_declaration=True)
627 sxw_contents = {'content.xml':odt, 'meta.xml':meta}
629 if report_xml.header:
630 #Add corporate header/footer
631 rml_file = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type))
633 rml = rml_file.read()
634 rml_parser = self.parser(cr, uid, self.name2, context=context)
635 rml_parser.parents = sxw_parents
636 rml_parser.tag = sxw_tag
637 objs = self.getObjects(cr, uid, ids, context)
638 rml_parser.set_context(objs, data, ids, report_xml.report_type)
639 rml_dom = self.preprocess_rml(etree.XML(rml),report_type)
640 create_doc = self.generators[report_type]
641 odt = create_doc(rml_dom,rml_parser.localcontext)
642 if report_xml.header:
643 rml_parser._add_header(odt)
644 odt = etree.tostring(odt, encoding='utf-8',
645 xml_declaration=True)
646 sxw_contents['styles.xml'] = odt
650 #created empty zip writing sxw contents to avoid duplication
651 sxw_out = StringIO.StringIO()
652 sxw_out_zip = zipfile.ZipFile(sxw_out, mode='w')
653 sxw_template_zip = zipfile.ZipFile (sxw_io, 'r')
654 for item in sxw_template_zip.infolist():
655 if item.filename not in sxw_contents:
656 buffer = sxw_template_zip.read(item.filename)
657 sxw_out_zip.writestr(item.filename, buffer)
658 for item_filename, buffer in sxw_contents.iteritems():
659 sxw_out_zip.writestr(item_filename, buffer)
660 sxw_template_zip.close()
662 final_op = sxw_out.getvalue()
665 return final_op, mime_type
667 def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
670 context = context.copy()
672 context['parents'] = html_parents
674 html = report_xml.report_rml_content
675 html_parser = self.parser(cr, uid, self.name2, context=context)
676 html_parser.parents = html_parents
677 html_parser.tag = sxw_tag
678 objs = self.getObjects(cr, uid, ids, context)
679 html_parser.set_context(objs, data, ids, report_type)
681 html_dom = etree.HTML(html)
682 html_dom = self.preprocess_rml(html_dom,'html2html')
684 create_doc = self.generators['html2html']
685 html = etree.tostring(create_doc(html_dom, html_parser.localcontext))
687 return html.replace('&','&').replace('<', '<').replace('>', '>').replace('</br>',''), report_type
689 def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None):
690 mako_html = report_xml.report_rml_content
691 html_parser = self.parser(cr, uid, self.name2, context)
692 objs = self.getObjects(cr, uid, ids, context)
693 html_parser.set_context(objs, data, ids, 'html')
694 create_doc = self.generators['makohtml2html']
695 html = create_doc(mako_html,html_parser.localcontext)
699 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: