# -*- coding: utf-8 -*- ############################################################################## # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################## from lxml import etree import StringIO import cStringIO import base64 from datetime import datetime import os import re import time from interface import report_rml import preprocess import logging import pooler import tools import zipfile import common from osv.fields import float as float_class, function as function_class DT_FORMAT = '%Y-%m-%d' DHM_FORMAT = '%Y-%m-%d %H:%M:%S' HM_FORMAT = '%H:%M:%S' rml_parents = { 'tr':1, 'li':1, 'story': 0, 'section': 0 } rml_tag="para" sxw_parents = { 'table-row': 1, 'list-item': 1, 'body': 0, 'section': 0, } html_parents = { 'tr' : 1, 'body' : 0, 'div' : 0 } sxw_tag = "p" rml2sxw = { 'para': 'p', } class _format(object): def set_value(self, cr, uid, name, object, field, lang_obj): self.object = object self._field = field self.name = name self.lang_obj = lang_obj class _float_format(float, _format): def __init__(self,value): super(_float_format, self).__init__() self.val = value def __str__(self): digits = 2 if hasattr(self,'_field') and getattr(self._field, 'digits', None): digits = self._field.digits[1] if hasattr(self, 'lang_obj'): return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True) return self.val class _int_format(int, _format): def __init__(self,value): super(_int_format, self).__init__() self.val = value and str(value) or str(0) def __str__(self): if hasattr(self,'lang_obj'): return self.lang_obj.format('%.d', self.name, True) return self.val class _date_format(str, _format): def __init__(self,value): super(_date_format, self).__init__() self.val = value and str(value) or '' def __str__(self): if self.val: if getattr(self,'name', None): date = datetime.strptime(self.name, DT_FORMAT) return date.strftime(str(self.lang_obj.date_format)) return self.val class _dttime_format(str, _format): def __init__(self,value): super(_dttime_format, self).__init__() self.val = value and str(value) or '' def __str__(self): if self.val and getattr(self,'name', None): return datetime.strptime(self.name, DHM_FORMAT)\ .strftime("%s %s"%(str(self.lang_obj.date_format), str(self.lang_obj.time_format))) return self.val _fields_process = { 'float': _float_format, 'date': _date_format, 'integer': _int_format, 'datetime' : _dttime_format } # # Context: {'node': node.dom} # class browse_record_list(list): def __init__(self, lst, context): super(browse_record_list, self).__init__(lst) self.context = context def __getattr__(self, name): res = browse_record_list([getattr(x,name) for x in self], self.context) return res def __str__(self): return "browse_record_list("+str(len(self))+")" class rml_parse(object): def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None): if not context: context={} self.cr = cr self.uid = uid self.pool = pooler.get_pool(cr.dbname) user = self.pool.get('res.users').browse(cr, uid, uid, context=context) self.localcontext = { 'user': user, 'company': user.company_id, 'repeatIn': self.repeatIn, 'setLang': self.setLang, 'setTag': self.setTag, 'removeParentNode': self.removeParentNode, 'format': self.format, 'formatLang': self.formatLang, 'logo' : user.company_id.logo, 'lang' : user.company_id.partner_id.lang, 'translate' : self._translate, 'setHtmlImage' : self.set_html_image, 'time' : time } self.localcontext.update(context) self.rml_header = user.company_id.rml_header self.rml_header2 = user.company_id.rml_header2 self.rml_header3 = user.company_id.rml_header3 self.logo = user.company_id.logo self.name = name self._node = None self.parents = parents self.tag = tag self._lang_cache = {} self.lang_dict = {} self.default_lang = {} self.lang_dict_called = False self._transl_regex = re.compile('(\[\[.+?\]\])') def setTag(self, oldtag, newtag, attrs=None): return newtag, attrs def format(self, text, oldtag=None): return text.strip() def removeParentNode(self, tag=None): raise GeneratorExit('Skip') def set_html_image(self,id,model=None,field=None,context=None): if not id : return '' if not model: model = 'ir.attachment' try : id = int(id) res = self.pool.get(model).read(self.cr,self.uid,id) if field : return res[field] elif model =='ir.attachment' : return res['datas'] else : return '' except Exception,e: return '' def setLang(self, lang): if not lang or self.default_lang.has_key(lang): if not lang: key = 'en_US' self.localcontext['lang'] = lang self.lang_dict_called = False for obj in self.objects: obj._context['lang'] = lang for table in obj._cache: for id in obj._cache[table]: self._lang_cache.setdefault(obj._context['lang'], {}).setdefault(table, {}).update(obj._cache[table][id]) if lang in self._lang_cache \ and table in self._lang_cache[lang] \ and id in self._lang_cache[lang][table]: obj._cache[table][id] = self._lang_cache[lang][table][id] else: obj._cache[table][id] = {'id': id} def _get_lang_dict(self): pool_lang = self.pool.get('res.lang') lang = self.localcontext.get('lang', 'en_US') or 'en_US' lang_ids = pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0] lang_obj = pool_lang.browse(self.cr,self.uid,lang_ids) self.lang_dict.update({'lang_obj':lang_obj,'date_format':lang_obj.date_format,'time_format':lang_obj.time_format}) self.default_lang[lang] = self.lang_dict.copy() return True def digits_fmt(self, obj=None, f=None, dp=None): digits = self.get_digits(obj, f, dp) return "%%.%df" % (digits, ) def get_digits(self, obj=None, f=None, dp=None): d = DEFAULT_DIGITS = 2 if dp: decimal_precision_obj = self.pool.get('decimal.precision') ids = decimal_precision_obj.search(self.cr, self.uid, [('name', '=', dp)]) if ids: d = decimal_precision_obj.browse(self.cr, self.uid, ids)[0].digits elif obj and f: res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS))) if isinstance(res_digits, tuple): d = res_digits[1] else: d = res_digits(self.cr)[1] elif (hasattr(obj, '_field') and\ isinstance(obj._field, (float_class, function_class)) and\ obj._field.digits): d = obj._field.digits[1] or DEFAULT_DIGITS return d def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False): """ Assuming 'Account' decimal.precision=3: formatLang(value) -> digits=2 (default) formatLang(value, digits=4) -> digits=4 formatLang(value, dp='Account') -> digits=3 formatLang(value, digits=5, dp='Account') -> digits=5 """ if digits is None: if dp: digits = self.get_digits(dp=dp) else: digits = self.get_digits(value) if isinstance(value, (str, unicode)) and not value: return '' if not self.lang_dict_called: self._get_lang_dict() self.lang_dict_called = True if date or date_time: if not str(value): return '' date_format = self.lang_dict['date_format'] parse_format = DT_FORMAT if date_time: value=value.split('.')[0] date_format = date_format + " " + self.lang_dict['time_format'] parse_format = DHM_FORMAT if not isinstance(value, time.struct_time): return time.strftime(date_format, time.strptime(value, parse_format)) else: date = datetime(*value.timetuple()[:6]) return date.strftime(date_format) return self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) def repeatIn(self, lst, name,nodes_parent=False): ret_lst = [] for id in lst: ret_lst.append({name:id}) return ret_lst def _translate(self,text): lang = self.localcontext['lang'] if lang and text and not text.isspace(): transl_obj = self.pool.get('ir.translation') piece_list = self._transl_regex.split(text) for pn in range(len(piece_list)): if not self._transl_regex.match(piece_list[pn]): source_string = piece_list[pn].replace('\n', ' ').strip() if len(source_string): translated_string = transl_obj._get_source(self.cr, self.uid, self.name, ('report', 'rml'), lang, source_string) if translated_string: piece_list[pn] = piece_list[pn].replace(source_string, translated_string) text = ''.join(piece_list) return text def _add_header(self, rml_dom, header='external'): if header=='internal': rml_head = self.rml_header2 elif header=='internal landscape': rml_head = self.rml_header3 else: rml_head = self.rml_header head_dom = etree.XML(rml_head) for tag in head_dom: found = rml_dom.find('.//'+tag.tag) if found is not None and len(found): if tag.get('position'): found.append(tag) else : found.getparent().replace(found,tag) return True def set_context(self, objects, data, ids, report_type = None): self.localcontext['data'] = data self.localcontext['objects'] = objects self.localcontext['digits_fmt'] = self.digits_fmt self.localcontext['get_digits'] = self.get_digits self.datas = data self.ids = ids self.objects = objects if report_type: if report_type=='odt' : self.localcontext.update({'name_space' :common.odt_namespace}) else: self.localcontext.update({'name_space' :common.sxw_namespace}) class report_sxw(report_rml, preprocess.report): def __init__(self, name, table, rml=False, parser=rml_parse, header='external', store=False): report_rml.__init__(self, name, table, rml, '') self.name = name self.parser = parser self.header = header self.store = store def getObjects(self, cr, uid, ids, context): table_obj = pooler.get_pool(cr.dbname).get(self.table) return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context, fields_process=_fields_process) def create(self, cr, uid, ids, data, context=None): pool = pooler.get_pool(cr.dbname) ir_obj = pool.get('ir.actions.report.xml') report_xml_ids = ir_obj.search(cr, uid, [('report_name', '=', self.name[7:])], context=context) if report_xml_ids: report_xml = ir_obj.browse(cr, uid, report_xml_ids[0], context=context) else: title = '' rml = tools.file_open(self.tmpl, subdir=None).read() report_type= data.get('report_type', 'pdf') class a(object): def __init__(self, *args, **argv): for key,arg in argv.items(): setattr(self, key, arg) report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header) report_xml.header = self.header report_type = report_xml.report_type if report_type in ['sxw','odt']: fnct = self.create_source_odt elif report_type in ['pdf','raw','txt','html']: fnct = self.create_source_pdf elif report_type=='html2html': fnct = self.create_source_html2html elif report_type=='mako2html': fnct = self.create_source_mako2html else: raise 'Unknown Report Type' fnct_ret = fnct(cr, uid, ids, data, report_xml, context) if not fnct_ret: return (False,False) return fnct_ret def create_source_odt(self, cr, uid, ids, data, report_xml, context=None): return self.create_single_odt(cr, uid, ids, data, report_xml, context or {}) def create_source_html2html(self, cr, uid, ids, data, report_xml, context=None): return self.create_single_html2html(cr, uid, ids, data, report_xml, context or {}) def create_source_mako2html(self, cr, uid, ids, data, report_xml, context=None): return self.create_single_mako2html(cr, uid, ids, data, report_xml, context or {}) def create_source_pdf(self, cr, uid, ids, data, report_xml, context=None): if not context: context={} pool = pooler.get_pool(cr.dbname) attach = report_xml.attachment if attach: objs = self.getObjects(cr, uid, ids, context) results = [] for obj in objs: aname = eval(attach, {'object':obj, 'time':time}) result = False if report_xml.attachment_use and aname and context.get('attachment_use', True): aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)]) if aids: brow_rec = pool.get('ir.attachment').browse(cr, uid, aids[0]) if not brow_rec.datas: continue d = base64.decodestring(brow_rec.datas) results.append((d,'pdf')) continue result = self.create_single_pdf(cr, uid, [obj.id], data, report_xml, context) if not result: return False if aname: try: name = aname+'.'+result[1] pool.get('ir.attachment').create(cr, uid, { 'name': aname, 'datas': base64.encodestring(result[0]), 'datas_fname': name, 'res_model': self.table, 'res_id': obj.id, }, context=context ) except Exception: #TODO: should probably raise a proper osv_except instead, shouldn't we? see LP bug #325632 logging.getLogger('report').error('Could not create saved report attachment', exc_info=True) results.append(result) if results: if results[0][1]=='pdf': from pyPdf import PdfFileWriter, PdfFileReader output = PdfFileWriter() for r in results: reader = PdfFileReader(cStringIO.StringIO(r[0])) for page in range(reader.getNumPages()): output.addPage(reader.getPage(page)) s = cStringIO.StringIO() output.write(s) return s.getvalue(), results[0][1] return self.create_single_pdf(cr, uid, ids, data, report_xml, context) def create_single_pdf(self, cr, uid, ids, data, report_xml, context=None): if not context: context={} logo = None context = context.copy() title = report_xml.name rml = report_xml.report_rml_content # if no rml file is found if not rml: return False rml_parser = self.parser(cr, uid, self.name2, context=context) objs = self.getObjects(cr, uid, ids, context) rml_parser.set_context(objs, data, ids, report_xml.report_type) processed_rml = etree.XML(rml) if report_xml.header: rml_parser._add_header(processed_rml, self.header) processed_rml = self.preprocess_rml(processed_rml,report_xml.report_type) if rml_parser.logo: logo = base64.decodestring(rml_parser.logo) create_doc = self.generators[report_xml.report_type] pdf = create_doc(etree.tostring(processed_rml),rml_parser.localcontext,logo,title.encode('utf8')) return (pdf, report_xml.report_type) def create_single_odt(self, cr, uid, ids, data, report_xml, context=None): if not context: context={} context = context.copy() report_type = report_xml.report_type context['parents'] = sxw_parents sxw_io = StringIO.StringIO(report_xml.report_sxw_content) sxw_z = zipfile.ZipFile(sxw_io, mode='r') rml = sxw_z.read('content.xml') meta = sxw_z.read('meta.xml') mime_type = sxw_z.read('mimetype') if mime_type == 'application/vnd.sun.xml.writer': mime_type = 'sxw' else : mime_type = 'odt' sxw_z.close() rml_parser = self.parser(cr, uid, self.name2, context=context) rml_parser.parents = sxw_parents rml_parser.tag = sxw_tag objs = self.getObjects(cr, uid, ids, context) rml_parser.set_context(objs, data, ids, mime_type) rml_dom_meta = node = etree.XML(meta) elements = node.findall(rml_parser.localcontext['name_space']["meta"]+"user-defined") for pe in elements: if pe.get(rml_parser.localcontext['name_space']["meta"]+"name"): if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 3": pe[0].text=data['id'] if pe.get(rml_parser.localcontext['name_space']["meta"]+"name") == "Info 4": pe[0].text=data['model'] meta = etree.tostring(rml_dom_meta, encoding='utf-8', xml_declaration=True) rml_dom = etree.XML(rml) body = rml_dom[-1] elements = [] key1 = rml_parser.localcontext['name_space']["text"]+"p" key2 = rml_parser.localcontext['name_space']["text"]+"drop-down" for n in rml_dom.iterdescendants(): if n.tag == key1: elements.append(n) if mime_type == 'odt': for pe in elements: e = pe.findall(key2) for de in e: pp=de.getparent() if de.text or de.tail: pe.text = de.text or de.tail for cnd in de: if cnd.text or cnd.tail: if pe.text: pe.text += cnd.text or cnd.tail else: pe.text = cnd.text or cnd.tail pp.remove(de) else: for pe in elements: e = pe.findall(key2) for de in e: pp = de.getparent() if de.text or de.tail: pe.text = de.text or de.tail for cnd in de: text = cnd.get("{http://openoffice.org/2000/text}value",False) if text: if pe.text and text.startswith('[['): pe.text += text elif text.startswith('[['): pe.text = text if de.getparent(): pp.remove(de) rml_dom = self.preprocess_rml(rml_dom, mime_type) create_doc = self.generators[mime_type] odt = etree.tostring(create_doc(rml_dom, rml_parser.localcontext), encoding='utf-8', xml_declaration=True) sxw_z = zipfile.ZipFile(sxw_io, mode='a') sxw_z.writestr('content.xml', odt) sxw_z.writestr('meta.xml', meta) if report_xml.header: #Add corporate header/footer rml = tools.file_open(os.path.join('base', 'report', 'corporate_%s_header.xml' % report_type)).read() rml_parser = self.parser(cr, uid, self.name2, context=context) rml_parser.parents = sxw_parents rml_parser.tag = sxw_tag objs = self.getObjects(cr, uid, ids, context) rml_parser.set_context(objs, data, ids, report_xml.report_type) rml_dom = self.preprocess_rml(etree.XML(rml),report_type) create_doc = self.generators[report_type] odt = create_doc(rml_dom,rml_parser.localcontext) if report_xml.header: rml_parser._add_header(odt) odt = etree.tostring(odt, encoding='utf-8', xml_declaration=True) sxw_z.writestr('styles.xml', odt) sxw_z.close() final_op = sxw_io.getvalue() sxw_io.close() return (final_op, mime_type) def create_single_html2html(self, cr, uid, ids, data, report_xml, context=None): if not context: context = {} context = context.copy() report_type = 'html' context['parents'] = html_parents html = report_xml.report_rml_content html_parser = self.parser(cr, uid, self.name2, context=context) html_parser.parents = html_parents html_parser.tag = sxw_tag objs = self.getObjects(cr, uid, ids, context) html_parser.set_context(objs, data, ids, report_type) html_dom = etree.HTML(html) html_dom = self.preprocess_rml(html_dom,'html2html') create_doc = self.generators['html2html'] html = etree.tostring(create_doc(html_dom, html_parser.localcontext)) return (html.replace('&','&').replace('<', '<').replace('>', '>').replace('
',''), report_type) def create_single_mako2html(self, cr, uid, ids, data, report_xml, context=None): mako_html = report_xml.report_rml_content html_parser = self.parser(cr, uid, self.name2, context) objs = self.getObjects(cr, uid, ids, context) html_parser.set_context(objs, data, ids, 'html') create_doc = self.generators['makohtml2html'] html = create_doc(mako_html,html_parser.localcontext) return (html,'html')