1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
23 from interface import report_rml
38 import xml.dom.minidom
41 DT_FORMAT = '%Y-%m-%d'
42 DHM_FORMAT = '%Y-%m-%d %H:%M:%S'
43 HM_FORMAT = '%H:%M:%S'
45 if not hasattr(locale, 'nl_langinfo'):
46 locale.nl_langinfo = lambda *a: '%x'
48 if not hasattr(locale, 'D_FMT'):
74 'af_ZA': 'Afrikaans_South Africa',
75 'sq_AL': 'Albanian_Albania',
76 'ar_SA': 'Arabic_Saudi Arabia',
77 'eu_ES': 'Basque_Spain',
78 'be_BY': 'Belarusian_Belarus',
79 'bs_BA': 'Serbian (Latin)',
80 'bg_BG': 'Bulgarian_Bulgaria',
81 'ca_ES': 'Catalan_Spain',
82 'hr_HR': 'Croatian_Croatia',
83 'zh_CN': 'Chinese_China',
84 'zh_TW': 'Chinese_Taiwan',
85 'cs_CZ': 'Czech_Czech Republic',
86 'da_DK': 'Danish_Denmark',
87 'nl_NL': 'Dutch_Netherlands',
88 'et_EE': 'Estonian_Estonia',
89 'fa_IR': 'Farsi_Iran',
90 'ph_PH': 'Filipino_Philippines',
91 'fi_FI': 'Finnish_Finland',
92 'fr_FR': 'French_France',
93 'fr_BE': 'French_France',
94 'fr_CH': 'French_France',
95 'fr_CA': 'French_France',
96 'ga': 'Scottish Gaelic',
97 'gl_ES': 'Galician_Spain',
98 'ka_GE': 'Georgian_Georgia',
99 'de_DE': 'German_Germany',
100 'el_GR': 'Greek_Greece',
101 'gu': 'Gujarati_India',
102 'he_IL': 'Hebrew_Israel',
104 'hu': 'Hungarian_Hungary',
105 'is_IS': 'Icelandic_Iceland',
106 'id_ID': 'Indonesian_indonesia',
107 'it_IT': 'Italian_Italy',
108 'ja_JP': 'Japanese_Japan',
111 'ko_KR': 'Korean_Korea',
113 'lt_LT': 'Lithuanian_Lithuania',
114 'lat': 'Latvian_Latvia',
115 'ml_IN': 'Malayalam_India',
116 'id_ID': 'Indonesian_indonesia',
118 'mn': 'Cyrillic_Mongolian',
119 'no_NO': 'Norwegian_Norway',
120 'nn_NO': 'Norwegian-Nynorsk_Norway',
121 'pl': 'Polish_Poland',
122 'pt_PT': 'Portuguese_Portugal',
123 'pt_BR': 'Portuguese_Brazil',
124 'ro_RO': 'Romanian_Romania',
125 'ru_RU': 'Russian_Russia',
127 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro',
128 'sk_SK': 'Slovak_Slovakia',
129 'sl_SI': 'Slovenian_Slovenia',
130 'es_ES': 'Spanish_Spain',
131 'sv_SE': 'Swedish_Sweden',
132 'ta_IN': 'English_Australia',
133 'th_TH': 'Thai_Thailand',
135 'tr_TR': 'Turkish_Turkey',
136 'uk_UA': 'Ukrainian_Ukraine',
137 'vi_VN': 'Vietnamese_Viet Nam',
140 class _format(object):
141 def set_value(self, name, object, field):
142 #super(_date_format, self).__init__(self)
146 lc, encoding = locale.getdefaultlocale()
149 if encoding == 'utf':
151 if encoding == 'cp1252':
153 lang = self.object._context.get('lang', 'en_US') or 'en_US'
156 locale.setlocale(locale.LC_ALL, _LOCALE2WIN32.get(lang, lang) + '.' + encoding)
158 locale.setlocale(locale.LC_ALL,str( lang + '.' + encoding))
160 netsvc.Logger().notifyChannel('report', netsvc.LOG_WARNING,
161 'report %s: unable to set locale "%s"' % (self.name,
162 self.object._context.get('lang', 'en_US') or 'en_US'))
165 class _float_format(float, _format):
167 if not self.object._context:
168 return locale.format('%f', self.name, True)
170 if hasattr(self._field, 'digits') and self._field.digits:
171 digit = self._field.digits[1]
172 return locale.format('%.' + str(digit) + 'f', self.name, True)
175 class _int_format(int, _format):
177 return locale.format('%d', self.name, True)
180 class _date_format(str, _format):
182 if not self.object._context:
187 datedata = time.strptime(self.name, DT_FORMAT)
188 return time.strftime(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y'),
196 'float': _float_format,
197 'date': _date_format,
198 'integer': _int_format
202 # Context: {'node': node.dom}
204 class browse_record_list(list):
205 def __init__(self, lst, context):
206 super(browse_record_list, self).__init__(lst)
207 self.context = context
209 def __getattr__(self, name):
210 res = browse_record_list([getattr(x,name) for x in self], self.context)
214 return "browse_record_list("+str(len(self))+")"
216 def repeatIn(self, name):
217 warnings.warn('Use repeatIn(object_list, \'variable\')', DeprecationWarning)
218 node = self.context['_node']
219 parents = self.context.get('parents', rml_parents)
222 if not node.parentNode:
224 node = node.parentNode
225 if node.nodeType == node.ELEMENT_NODE and node.localName in parents:
231 for i in range(1,len(self)):
232 newnode = parent_node.cloneNode(1)
233 n = parent_node.parentNode
234 n.insertBefore(newnode, parent_node)
235 nodes.append((i,newnode))
237 self.context[name] = self[i]
238 self.context['_self']._parse_node(node)
241 class rml_parse(object):
242 def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None):
247 self.pool = pooler.get_pool(cr.dbname)
248 user = self.pool.get('res.users').browse(cr, uid, uid, fields_process=_fields_process)
249 self.localcontext = {
251 'company': user.company_id,
252 'repeatIn': self.repeatIn,
253 'setLang': self.setLang,
254 'setTag': self.setTag,
255 'removeParentNode': self.removeParentNode,
256 'format': self.format,
257 'formatLang': self.formatLang,
258 'logo' : user.company_id.logo,
259 'lang' : user.company_id.partner_id.lang,
261 self.localcontext.update(context)
262 self.rml_header = user.company_id.rml_header
263 self.rml_header2 = user.company_id.rml_header2
264 self.logo = user.company_id.logo
266 self._regex = re.compile('\[\[(.+?)\]\]')
267 self._transl_regex = re.compile('(\[\[.+?\]\])')
269 self.parents = parents
271 self._lang_cache = {}
274 def setTag(self, oldtag, newtag, attrs=None):
277 node = self._find_parent(self._node, [oldtag])
279 node.tagName = newtag
280 for key, val in attrs.items():
281 node.setAttribute(key, val)
284 def format(self, text, oldtag=None):
288 node = self._find_parent(self._node, [oldtag])
292 ns = node.nextSibling
296 lst = tools.ustr(text).split('\n')
297 if not (text and lst):
300 for i in range(len(lst)):
301 newnode = node.cloneNode(1)
302 newnode.tagName=rml_tag
303 newnode.childNodes[0].data = lst[i]
305 pp.insertBefore(newnode, ns)
307 pp.appendChild(newnode)
308 nodes.append((i, newnode))
310 def removeParentNode(self, tag=None):
313 if self.tag == sxw_tag and rml2sxw.get(tag, False):
315 node = self._find_parent(self._node, [tag])
317 parentNode = node.parentNode
318 parentNode.removeChild(node)
319 self._node = parentNode
321 def setLang(self, lang):
322 self.localcontext['lang'] = lang
323 for obj in self.objects:
324 obj._context['lang'] = lang
325 for table in obj._cache:
326 for id in obj._cache[table]:
327 self._lang_cache.setdefault(obj._context['lang'], {}).setdefault(table,
328 {}).update(obj._cache[table][id])
329 if lang in self._lang_cache \
330 and table in self._lang_cache[lang] \
331 and id in self._lang_cache[lang][table]:
332 obj._cache[table][id] = self._lang_cache[lang][table][id]
334 obj._cache[table][id] = {'id': id}
337 def formatLang(self, value, digits=2, date=False,date_time=False, grouping=True, monetary=False, currency=None):
338 if (not isinstance(value,float)) and (not value):
340 pool_lang=self.pool.get('res.lang')
341 lang = self.localcontext.get('lang', 'en_US') or 'en_US'
342 lang_obj = pool_lang.browse(self.cr,self.uid,pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0])
343 if date or date_time:
344 date_format = lang_obj.date_format
346 date_format = lang_obj.date_format + " " + lang_obj.time_format
347 if not isinstance(value, time.struct_time):
348 # assume string, parse it
349 if len(str(value)) == 10:
350 # length of date like 2001-01-01 is ten
351 # assume format '%Y-%m-%d'
352 date = mx.DateTime.strptime(value,DT_FORMAT)
354 # assume format '%Y-%m-%d %H:%M:%S'
355 value = str(value)[:19]
356 date = mx.DateTime.strptime(str(value),DHM_FORMAT)
358 date = mx.DateTime.DateTime(*(value.timetuple()[:6]))
359 return date.strftime(date_format)
360 return lang_obj.format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
362 # def formatLang(self, value, digit=2, date=False):
365 # lc, encoding = locale.getdefaultlocale()
368 # if encoding == 'utf':
370 # if encoding == 'cp1252':
372 # lang = self.localcontext.get('lang', 'en_US') or 'en_US'
374 # if os.name == 'nt':
375 # locale.setlocale(locale.LC_ALL, _LOCALE2WIN32.get(lang, lang) + '.' + encoding)
377 # locale.setlocale(locale.LC_ALL, lang + '.' + encoding)
379 # netsvc.Logger().notifyChannel('report', netsvc.LOG_WARNING,
380 # 'report %s: unable to set locale "%s"' % (self.name,
381 # self.localcontext.get('lang', 'en_US') or 'en_US'))
383 # date = time.strptime(value, DT_FORMAT)
384 # return time.strftime(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y'),
386 # return locale.format('%.' + str(digit) + 'f', value, True)
388 def repeatIn(self, lst, name, nodes_parent=False):
390 node = self._find_parent(self._node, nodes_parent or self.parents)
393 ns = node.nextSibling
400 for i in range(len(lst)):
401 newnode = node.cloneNode(1)
403 pp.insertBefore(newnode, ns)
405 pp.appendChild(newnode)
406 nodes.append((i, newnode))
407 for i, node in nodes:
408 self.node_context[node] = {name: lst[i]}
411 def _eval(self, expr):
413 res = eval(expr, self.localcontext)
414 if (res is None) or (res=='') or (res is False):
417 import traceback, sys
418 tb_s = reduce(lambda x, y: x+y, traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
419 netsvc.Logger().notifyChannel('report', netsvc.LOG_ERROR,
420 'report %s:\n%s\n%s\nexpr: %s' % (self.name, tb_s, str(e),
421 expr.encode('utf-8')))
425 def _find_parent(self, node, parents):
427 if not node.parentNode:
429 node = node.parentNode
430 if node.nodeType == node.ELEMENT_NODE and node.localName in parents:
434 def _parse_text(self, text, level=None):
437 res = self._regex.findall(text)
440 # the "split [[]] if not match [[]]" is not very nice, but I
441 # don't see how I could do it better...
442 # what I'd like to do is a re.sub(NOT pattern, func, string)
443 # but I don't know how to do that...
444 # translate the RML file
445 if 'lang' in self.localcontext:
446 lang = self.localcontext['lang']
447 if lang and text and not text.isspace():
448 transl_obj = self.pool.get('ir.translation')
449 piece_list = self._transl_regex.split(text)
450 for pn in range(len(piece_list)):
451 if not self._transl_regex.match(piece_list[pn]):
452 source_string = piece_list[pn].replace('\n', ' ').strip()
453 if len(source_string):
454 translated_string = transl_obj._get_source(self.cr, self.uid, self.name, 'rml', lang, source_string)
455 if translated_string:
456 piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
457 text = ''.join(piece_list)
459 newtext = self._eval(key)
460 for i in range(len(level)):
461 if isinstance(newtext, list):
462 newtext = newtext[level[i]]
463 if isinstance(newtext, list):
464 todo.append((key, newtext))
466 # if there are two [[]] blocks the same, it will replace both
467 # but it's ok because it should evaluate to the same thing
469 newtext = tools.ustr(newtext)
470 text = text.replace('[['+key+']]', newtext)
471 self._node.data = text
473 for key, newtext in todo:
474 parent_node = self._find_parent(self._node, parents)
475 assert parents.get(parent_node.localName, False), 'No parent node found !'
476 nodes = [parent_node]
477 for i in range(len(newtext) - 1):
478 newnode = parent_node.cloneNode(1)
479 if parents.get(parent_node.localName, False):
480 n = parent_node.parentNode
481 parent_node.parentNode.insertAfter(newnode, parent_node)
482 nodes.append(newnode)
486 def _parse_node(self):
489 if self._node.nodeType==self._node.ELEMENT_NODE:
490 if self._node.hasAttribute('expr'):
491 newattrs = self._eval(self._node.getAttribute('expr'))
492 for key,val in newattrs.items():
493 self._node.setAttribute(key,val)
495 if self._node.hasChildNodes():
496 self._node = self._node.firstChild
497 elif self._node.nextSibling:
498 self._node = self._node.nextSibling
500 while self._node and not self._node.nextSibling:
501 self._node = self._node.parentNode
504 self._node = self._node.nextSibling
505 if self._node in self.node_context:
506 self.localcontext.update(self.node_context[self._node])
507 if self._node.nodeType in (self._node.CDATA_SECTION_NODE, self._node.TEXT_NODE):
508 # if self._node in self.already:
509 # self.already[self._node] += 1
510 # print "second pass!", self.already[self._node], '---%s---' % self._node.data
512 # self.already[self._node] = 0
513 self._parse_text(self._node.data, level)
516 def _find_node(self, node, localname):
517 if node.localName==localname:
519 for tag in node.childNodes:
520 if tag.nodeType==tag.ELEMENT_NODE:
521 found = self._find_node(tag, localname)
526 def _add_header(self, node, header=1):
528 rml_head = self.rml_header2
530 rml_head = self.rml_header
532 # Refactor this patch, to use the minidom interface
533 if self.logo and (rml_head.find('company.logo')<0 or rml_head.find('<image')<0) and rml_head.find('<!--image')<0:
534 rml_head = rml_head.replace('<pageGraphics>','''<pageGraphics> <image x="10" y="26cm" height="70" width="90" >[[company.logo]] </image> ''')
535 if not self.logo and rml_head.find('company.logo')>=0:
536 rml_head = rml_head.replace('<image','<!--image')
537 rml_head = rml_head.replace('</image>','</image-->')
539 head_dom = xml.dom.minidom.parseString(rml_head)
540 #for frame in head_dom.getElementsByTagName('frame'):
541 # frame.parentNode.removeChild(frame)
542 node2 = head_dom.documentElement
543 for tag in node2.childNodes:
544 if tag.nodeType==tag.ELEMENT_NODE:
545 found = self._find_node(node, tag.localName)
546 # rml_frames = found.getElementsByTagName('frame')
548 if tag.hasAttribute('position') and (tag.getAttribute('position')=='inside'):
549 found.appendChild(tag)
551 found.parentNode.replaceChild(tag, found)
552 # for frame in rml_frames:
553 # tag.appendChild(frame)
556 def preprocess(self, objects, data, ids):
557 self.localcontext['data'] = data
558 self.localcontext['objects'] = objects
561 self.objects = objects
563 def _parse(self, rml_dom, objects, data, header=0):
564 self.node_context = {}
566 self._node = self.dom.documentElement
568 self._add_header(self._node, header)
570 res = self.dom.documentElement.toxml('utf-8')
573 class report_sxw(report_rml):
574 def __init__(self, name, table, rml, parser=rml_parse, header=True, store=False):
575 report_rml.__init__(self, name, table, rml, '')
581 def getObjects(self, cr, uid, ids, context):
582 table_obj = pooler.get_pool(cr.dbname).get(self.table)
583 return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context,
584 fields_process=_fields_process)
586 def create(self, cr, uid, ids, data, context=None):
589 pool = pooler.get_pool(cr.dbname)
590 ir_obj = pool.get('ir.actions.report.xml')
591 report_xml_ids = ir_obj.search(cr, uid,
592 [('report_name', '=', self.name[7:])], context=context)
595 report_xml = ir_obj.browse(cr, uid, report_xml_ids[0],
597 attach = report_xml.attachment
599 ir_menu_report_obj = pool.get('ir.ui.menu')
600 report_menu_ids = ir_menu_report_obj.search(cr, uid,
601 [('id', 'in', ids)], context=context)
604 report_name = ir_menu_report_obj.browse(cr, uid, report_menu_ids[0],
606 title = report_name.name
607 rml = tools.file_open(self.tmpl, subdir=None).read()
608 report_type= data.get('report_type', 'pdf')
610 def __init__(self, *args, **argv):
611 for key,arg in argv.items():
612 setattr(self, key, arg)
613 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
617 objs = self.getObjects(cr, uid, ids, context)
620 aname = eval(attach, {'object':obj, 'time':time})
622 if report_xml.attachment_use and aname and context.get('attachment_use', True):
623 aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
625 d = base64.decodestring(pool.get('ir.attachment').browse(cr, uid, aids[0]).datas)
626 results.append((d,'pdf'))
629 result = self.create_single(cr, uid, [obj.id], data, report_xml, context)
631 name = aname+'.'+result[1]
632 pool.get('ir.attachment').create(cr, uid, {
634 'datas': base64.encodestring(result[0]),
636 'res_model': self.table,
641 results.append(result)
643 if results[0][1]=='pdf':
644 from pyPdf import PdfFileWriter, PdfFileReader
646 output = PdfFileWriter()
648 reader = PdfFileReader(cStringIO.StringIO(r[0]))
649 for page in range(reader.getNumPages()):
650 output.addPage(reader.getPage(page))
651 s = cStringIO.StringIO()
653 return s.getvalue(), results[0][1]
654 return self.create_single(cr, uid, ids, data, report_xml, context)
656 def create_single(self, cr, uid, ids, data, report_xml, context={}):
658 context = context.copy()
659 pool = pooler.get_pool(cr.dbname)
660 want_header = self.header
661 title = report_xml.name
662 attach = report_xml.attachment
663 report_type = report_xml.report_type
664 want_header = report_xml.header
666 if report_type in ['sxw','odt']:
667 context['parents'] = sxw_parents
668 sxw_io = StringIO.StringIO(report_xml.report_sxw_content)
669 sxw_z = zipfile.ZipFile(sxw_io, mode='r')
670 rml = sxw_z.read('content.xml')
671 meta = sxw_z.read('meta.xml')
673 rml_parser = self.parser(cr, uid, self.name2, context)
674 rml_parser.parents = sxw_parents
675 rml_parser.tag = sxw_tag
676 objs = self.getObjects(cr, uid, ids, context)
677 rml_parser.preprocess(objs, data, ids)
678 rml_dom = xml.dom.minidom.parseString(rml)
680 node = rml_dom.documentElement
682 elements = node.getElementsByTagName("text:p")
685 e = pe.getElementsByTagName("text:drop-down")
688 for cnd in de.childNodes:
689 if cnd.nodeType in (cnd.CDATA_SECTION_NODE, cnd.TEXT_NODE):
693 # Add Information : Resource ID and Model
694 rml_dom_meta = xml.dom.minidom.parseString(meta)
695 node = rml_dom_meta.documentElement
696 elements = node.getElementsByTagName("meta:user-defined")
698 if pe.hasAttribute("meta:name"):
699 if pe.getAttribute("meta:name") == "Info 3":
700 pe.childNodes[0].data=data['id']
701 if pe.getAttribute("meta:name") == "Info 4":
702 pe.childNodes[0].data=data['model']
703 meta = rml_dom_meta.documentElement.toxml('utf-8')
705 rml2 = rml_parser._parse(rml_dom, objs, data, header=want_header)
706 sxw_z = zipfile.ZipFile(sxw_io, mode='a')
707 sxw_z.writestr('content.xml', "<?xml version='1.0' encoding='UTF-8'?>" + \
709 sxw_z.writestr('meta.xml', "<?xml version='1.0' encoding='UTF-8'?>" + \
713 #Add corporate header/footer
714 if report_type=='odt':
715 rml = tools.file_open('custom/corporate_odt_header.xml').read()
716 if report_type=='sxw':
717 rml = tools.file_open('custom/corporate_sxw_header.xml').read()
718 rml_parser = self.parser(cr, uid, self.name2, context)
719 rml_parser.parents = sxw_parents
720 rml_parser.tag = sxw_tag
721 objs = self.getObjects(cr, uid, ids, context)
722 rml_parser.preprocess(objs, data, ids)
723 rml_dom = xml.dom.minidom.parseString(rml)
724 rml2 = rml_parser._parse(rml_dom, objs, data, header=want_header)
725 sxw_z.writestr('styles.xml',"<?xml version='1.0' encoding='UTF-8'?>" + \
728 rml2 = sxw_io.getvalue()
731 rml = report_xml.report_rml_content
732 context['parents'] = rml_parents
733 rml_parser = self.parser(cr, uid, self.name2, context)
734 rml_parser.parents = rml_parents
735 rml_parser.tag = rml_tag
736 objs = self.getObjects(cr, uid, ids, context)
737 rml_parser.preprocess(objs, data, ids)
738 rml_dom = xml.dom.minidom.parseString(rml)
739 rml2 = rml_parser._parse(rml_dom, objs, data, header=want_header)
741 logo = base64.decodestring(rml_parser.logo)
743 create_doc = self.generators[report_type]
744 pdf = create_doc(rml2, logo, title.encode('utf8'))
746 return (pdf, report_type)