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 import SUPERUSER_ID
38 from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
39 from openerp.tools.translate import _
40 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
42 _logger = logging.getLogger(__name__)
71 def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
72 return len((datetime.now()).strftime(date_format))
74 class _format(object):
75 def set_value(self, cr, uid, name, object, field, lang_obj):
79 self.lang_obj = lang_obj
81 class _float_format(float, _format):
82 def __init__(self,value):
83 super(_float_format, self).__init__()
84 self.val = value or 0.0
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)
94 class _int_format(int, _format):
95 def __init__(self,value):
96 super(_int_format, self).__init__()
100 if hasattr(self,'lang_obj'):
101 return self.lang_obj.format('%.d', self.name, True)
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 ''
111 if getattr(self,'name', None):
112 date = datetime.strptime(self.name[:get_date_length()], DEFAULT_SERVER_DATE_FORMAT)
113 return date.strftime(self.lang_obj.date_format.encode('utf-8'))
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 ''
122 if self.val and getattr(self,'name', None):
123 return datetime.strptime(self.name, DEFAULT_SERVER_DATETIME_FORMAT)\
124 .strftime("%s %s"%((self.lang_obj.date_format).encode('utf-8'),
125 (self.lang_obj.time_format).encode('utf-8')))
130 'float': _float_format,
131 'date': _date_format,
132 'integer': _int_format,
133 'datetime' : _dttime_format
137 # Context: {'node': node.dom}
139 class browse_record_list(list):
140 def __init__(self, lst, context):
141 super(browse_record_list, self).__init__(lst)
142 self.context = context
144 def __getattr__(self, name):
145 res = browse_record_list([getattr(x,name) for x in self], self.context)
149 return "browse_record_list("+str(len(self))+")"
151 class rml_parse(object):
152 def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None):
157 self.pool = openerp.registry(cr.dbname)
158 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
159 self.localcontext = {
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,
173 'display_address': self.display_address,
174 # more context members are setup in setCompany() below:
178 self.setCompany(user.company_id)
179 self.localcontext.update(context)
182 self.parents = parents
184 self._lang_cache = {}
186 self.default_lang = {}
187 self.lang_dict_called = False
188 self._transl_regex = re.compile('(\[\[.+?\]\])')
190 def setTag(self, oldtag, newtag, attrs=None):
193 def _ellipsis(self, char, size=100, truncation_str='...'):
196 if len(char) <= size:
198 return char[:size-len(truncation_str)] + truncation_str
200 def setCompany(self, company_id):
202 self.localcontext['company'] = company_id
203 self.localcontext['logo'] = company_id.logo
204 self.rml_header = company_id.rml_header
205 self.rml_header2 = company_id.rml_header2
206 self.rml_header3 = company_id.rml_header3
207 self.logo = company_id.logo
209 def _strip_name(self, name, maxlen=50):
210 return self._ellipsis(name, maxlen)
212 def format(self, text, oldtag=None):
215 def removeParentNode(self, tag=None):
216 raise GeneratorExit('Skip')
218 def set_html_image(self,id,model=None,field=None,context=None):
222 model = 'ir.attachment'
225 res = self.pool[model].read(self.cr,self.uid,id)
228 elif model =='ir.attachment' :
235 def setLang(self, lang):
236 self.localcontext['lang'] = lang
237 self.lang_dict_called = False
238 for obj in self.objects:
239 obj._context['lang'] = lang
241 def _get_lang_dict(self):
242 pool_lang = self.pool['res.lang']
243 lang = self.localcontext.get('lang', 'en_US') or 'en_US'
244 lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0]
245 lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids)
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['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] or DEFAULT_DIGITS
271 def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False):
273 Assuming 'Account' decimal.precision=3:
274 formatLang(value) -> digits=2 (default)
275 formatLang(value, digits=4) -> digits=4
276 formatLang(value, dp='Account') -> digits=3
277 formatLang(value, digits=5, dp='Account') -> digits=5
281 digits = self.get_digits(dp=dp)
283 digits = self.get_digits(value)
285 if isinstance(value, (str, unicode)) and not value:
288 if not self.lang_dict_called:
289 self._get_lang_dict()
290 self.lang_dict_called = True
292 if date or date_time:
296 date_format = self.lang_dict['date_format']
297 parse_format = DEFAULT_SERVER_DATE_FORMAT
299 value = value.split('.')[0]
300 date_format = date_format + " " + self.lang_dict['time_format']
301 parse_format = DEFAULT_SERVER_DATETIME_FORMAT
302 if isinstance(value, basestring):
303 # FIXME: the trimming is probably unreliable if format includes day/month names
304 # and those would need to be translated anyway.
305 date = datetime.strptime(value[:get_date_length(parse_format)], parse_format)
306 elif isinstance(value, time.struct_time):
307 date = datetime(*value[:6])
309 date = datetime(*value.timetuple()[:6])
311 # Convert datetime values to the expected client/context timezone
312 date = datetime_field.context_timestamp(self.cr, self.uid,
314 context=self.localcontext)
315 return date.strftime(date_format.encode('utf-8'))
317 res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
319 if currency_obj.position == 'after':
320 res='%s %s'%(res,currency_obj.symbol)
321 elif currency_obj and currency_obj.position == 'before':
322 res='%s %s'%(currency_obj.symbol, res)
325 def display_address(self, address_browse_record):
326 return self.pool['res.partner']._display_address(self.cr, self.uid, address_browse_record)
328 def repeatIn(self, lst, name,nodes_parent=False):
331 ret_lst.append({name:id})
334 def _translate(self,text):
335 lang = self.localcontext['lang']
336 if lang and text and not text.isspace():
337 transl_obj = self.pool['ir.translation']
338 piece_list = self._transl_regex.split(text)
339 for pn in range(len(piece_list)):
340 if not self._transl_regex.match(piece_list[pn]):
341 source_string = piece_list[pn].replace('\n', ' ').strip()
342 if len(source_string):
343 translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string)
344 if translated_string:
345 piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
346 text = ''.join(piece_list)
349 def _add_header(self, rml_dom, header='external'):
350 if header=='internal':
351 rml_head = self.rml_header2
352 elif header=='internal landscape':
353 rml_head = self.rml_header3
355 rml_head = self.rml_header
357 head_dom = etree.XML(rml_head)
359 found = rml_dom.find('.//'+tag.tag)
360 if found is not None and len(found):
361 if tag.get('position'):
364 found.getparent().replace(found,tag)
367 def set_context(self, objects, data, ids, report_type = None):
368 self.localcontext['data'] = data
369 self.localcontext['objects'] = objects
370 self.localcontext['digits_fmt'] = self.digits_fmt
371 self.localcontext['get_digits'] = self.get_digits
374 self.objects = objects
376 if report_type=='odt' :
377 self.localcontext.update({'name_space' :common.odt_namespace})
379 self.localcontext.update({'name_space' :common.sxw_namespace})
381 # WARNING: the object[0].exists() call below is slow but necessary because
382 # some broken reporting wizards pass incorrect IDs (e.g. ir.ui.menu ids)
383 if objects and len(objects) == 1 and \
384 objects[0].exists() and 'company_id' in objects[0] and objects[0].company_id:
385 # When we print only one record, we can auto-set the correct
386 # company in the localcontext. For other cases the report
387 # will have to call setCompany() inside the main repeatIn loop.
388 self.setCompany(objects[0].company_id)
390 class report_sxw(report_rml, preprocess.report):
392 The register=True kwarg has been added to help remove the
393 openerp.netsvc.LocalService() indirection and the related
394 openerp.report.interface.report_int._reports dictionary:
395 report_sxw registered in XML with auto=False are also registered in Python.
396 In that case, they are registered in the above dictionary. Since
397 registration is automatically done upon instanciation, and that
398 instanciation is needed before rendering, a way was needed to
399 instanciate-without-register a report. In the future, no report
400 should be registered in the above dictionary and it will be dropped.
402 def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False, register=True):
403 report_rml.__init__(self, name, table, rml, '', register=register)
408 self.internal_header=False
409 if header=='internal' or header=='internal landscape':
410 self.internal_header=True
412 def getObjects(self, cr, uid, ids, context):
413 table_obj = openerp.registry(cr.dbname)[self.table]
414 return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process)
416 def create(self, cr, uid, ids, data, context=None):
419 if self.internal_header:
420 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 registry['res.font'].font_scan(cr, SUPERUSER_ID, lazy=True, context=context)
428 report_xml_ids = ir_obj.search(cr, uid,
429 [('report_name', '=', self.name[7:])], context=context)
431 report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context)
434 report_file = tools.file_open(self.tmpl, subdir=None)
436 rml = report_file.read()
437 report_type= data.get('report_type', 'pdf')
439 def __init__(self, *args, **argv):
440 for key,arg in argv.items():
441 setattr(self, key, arg)
442 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
445 if report_xml.header:
446 report_xml.header = self.header
447 report_type = report_xml.report_type
448 if report_type in ['sxw','odt']:
449 fnct = self.create_source_odt
450 elif report_type in ['pdf','raw','txt','html']:
451 fnct = self.create_source_pdf
452 elif report_type=='html2html':
453 fnct = self.create_source_html2html
454 elif report_type=='mako2html':
455 fnct = self.create_source_mako2html
457 raise NotImplementedError(_('Unknown report type: %s') % report_type)
458 fnct_ret = fnct(cr, uid, ids, data, report_xml, context)
463 def create_source_odt(self, cr, uid, ids, data, report_xml, context=None):
464 return self.create_single_odt(cr, uid, ids, data, report_xml, context or {})
466 def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None):
467 return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {})
469 def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None):
470 return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {})
472 def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None):
475 registry = openerp.registry(cr.dbname)
476 attach = report_xml.attachment
478 objs = self.getObjects(cr, uid, ids, context)
481 aname = eval(attach, {'object':obj, 'time':time})
483 if report_xml.attachment_use and aname and context.get('attachment_use', True):
484 aids = registry['ir.attachment'].search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
486 brow_rec = registry['ir.attachment'].browse(cr, uid, aids[0])
487 if not brow_rec.datas:
489 d = base64.decodestring(brow_rec.datas)
490 results.append((d,'pdf'))
492 result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context)
497 name = aname+'.'+result[1]
498 # Remove the default_type entry from the context: this
499 # is for instance used on the account.account_invoices
500 # and is thus not intended for the ir.attachment type
503 ctx.pop('default_type', None)
504 registry['ir.attachment'].create(cr, uid, {
506 'datas': base64.encodestring(result[0]),
508 'res_model': self.table,
513 #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632
514 _logger.error('Could not create saved report attachment', exc_info=True)
515 results.append(result)
517 if results[0][1]=='pdf':
518 from pyPdf import PdfFileWriter, PdfFileReader
519 output = PdfFileWriter()
521 reader = PdfFileReader(cStringIO.StringIO(r[0]))
522 for page in range(reader.getNumPages()):
523 output.addPage(reader.getPage(page))
524 s = cStringIO.StringIO()
526 return s.getvalue(), results[0][1]
527 return self.create_single_pdf(cr, uid, ids, data, report_xml, context)
529 def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None):
533 context = context.copy()
534 title = report_xml.name
535 rml = report_xml.report_rml_content
536 # if no rml file is found
539 rml_parser = self.parser(cr, uid, self.name2, context=context)
540 objs = self.getObjects(cr, uid, ids, context)
541 rml_parser.set_context(objs, data, ids, report_xml.report_type)
542 processed_rml = etree.XML(rml)
543 if report_xml.header:
544 rml_parser._add_header(processed_rml, self.header)
545 processed_rml = self.preprocess_rml(processed_rml,report_xml.report_type)
547 logo = base64.decodestring(rml_parser.logo)
548 create_doc = self.generators[report_xml.report_type]
549 pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8'))
550 return pdf, report_xml.report_type
552 def create_single_odt(self, cr, uid, ids, data, report_xml, context=None):
555 context = context.copy()
556 report_type = report_xml.report_type
557 context['parents'] = sxw_parents
558 binary_report_content = report_xml.report_sxw_content
559 if isinstance(report_xml.report_sxw_content, unicode):
560 # if binary content was passed as unicode, we must
561 # re-encode it as a 8-bit string using the pass-through
562 # 'latin1' encoding, to restore the original byte values.
563 # See also osv.fields.sanitize_binary_value()
564 binary_report_content = report_xml.report_sxw_content.encode("latin1")
566 sxw_io = StringIO.StringIO(binary_report_content)
567 sxw_z = zipfile.ZipFile(sxw_io, mode='r')
568 rml = sxw_z.read('content.xml')
569 meta = sxw_z.read('meta.xml')
570 mime_type = sxw_z.read('mimetype')
571 if mime_type == 'application/vnd.sun.xml.writer':
577 rml_parser = self.parser(cr, uid, self.name2, context=context)
578 rml_parser.parents = sxw_parents
579 rml_parser.tag = sxw_tag
580 objs = self.getObjects(cr, uid, ids, context)
581 rml_parser.set_context(objs, data, ids, mime_type)
583 rml_dom_meta = node = etree.XML(meta)
584 elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined")
586 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"):
587 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3":
588 pe[0].text=data['id']
589 if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4":
590 pe[0].text=data['model']
591 meta = etree.tostring(rml_dom_meta, encoding='utf-8',
592 xml_declaration=True)
594 rml_dom = etree.XML(rml)
596 key1 = rml_parser.localcontext['name_space']["text"]+"p"
597 key2 = rml_parser.localcontext['name_space']["text"]+"drop-down"
598 for n in rml_dom.iterdescendants():
601 if mime_type == 'odt':
606 if de.text or de.tail:
607 pe.text = de.text or de.tail
609 if cnd.text or cnd.tail:
611 pe.text += cnd.text or cnd.tail
613 pe.text = cnd.text or cnd.tail
620 if de.text or de.tail:
621 pe.text = de.text or de.tail
623 text = cnd.get("{http://openoffice.org/2000/text}value",False)
625 if pe.text and text.startswith('[['):
627 elif text.startswith('[['):
632 rml_dom = self.preprocess_rml(rml_dom, mime_type)
633 create_doc = self.generators[mime_type]
634 odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext),
635 encoding='utf-8', xml_declaration=True)
636 sxw_contents = {'content.xml':odt, 'meta.xml':meta}
638 if report_xml.header:
639 #Add corporate header/footer
640 rml_file = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type))
642 rml = rml_file.read()
643 rml_parser = self.parser(cr, uid, self.name2, context=context)
644 rml_parser.parents = sxw_parents
645 rml_parser.tag = sxw_tag
646 objs = self.getObjects(cr, uid, ids, context)
647 rml_parser.set_context(objs, data, ids, report_xml.report_type)
648 rml_dom = self.preprocess_rml(etree.XML(rml),report_type)
649 create_doc = self.generators[report_type]
650 odt = create_doc(rml_dom,rml_parser.localcontext)
651 if report_xml.header:
652 rml_parser._add_header(odt)
653 odt = etree.tostring(odt, encoding='utf-8',
654 xml_declaration=True)
655 sxw_contents['styles.xml'] = odt
659 #created empty zip writing sxw contents to avoid duplication
660 sxw_out = StringIO.StringIO()
661 sxw_out_zip = zipfile.ZipFile(sxw_out, mode='w')
662 sxw_template_zip = zipfile.ZipFile (sxw_io, 'r')
663 for item in sxw_template_zip.infolist():
664 if item.filename not in sxw_contents:
665 buffer = sxw_template_zip.read(item.filename)
666 sxw_out_zip.writestr(item.filename, buffer)
667 for item_filename, buffer in sxw_contents.iteritems():
668 sxw_out_zip.writestr(item_filename, buffer)
669 sxw_template_zip.close()
671 final_op = sxw_out.getvalue()
674 return final_op, mime_type
676 def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None):
679 context = context.copy()
681 context['parents'] = html_parents
683 html = report_xml.report_rml_content
684 html_parser = self.parser(cr, uid, self.name2, context=context)
685 html_parser.parents = html_parents
686 html_parser.tag = sxw_tag
687 objs = self.getObjects(cr, uid, ids, context)
688 html_parser.set_context(objs, data, ids, report_type)
690 html_dom = etree.HTML(html)
691 html_dom = self.preprocess_rml(html_dom,'html2html')
693 create_doc = self.generators['html2html']
694 html = etree.tostring(create_doc(html_dom, html_parser.localcontext))
696 return html.replace('&','&').replace('<', '<').replace('>', '>').replace('</br>',''), report_type
698 def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None):
699 mako_html = report_xml.report_rml_content
700 html_parser = self.parser(cr, uid, self.name2, context)
701 objs = self.getObjects(cr, uid, ids, context)
702 html_parser.set_context(objs, data, ids, 'html')
703 create_doc = self.generators['makohtml2html']
704 html = create_doc(mako_html,html_parser.localcontext)
708 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: