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, cr, uid, name, object, field):
142 #super(_date_format, self).__init__(self)
143 pool_lang = pooler.get_pool(cr.dbname).get('res.lang')
147 # lc, encoding = locale.getdefaultlocale()
150 # if encoding == 'utf':
152 # if encoding == 'cp1252':
154 lang = self.object._context.get('lang', 'en_US') or 'en_US'
155 self.lang_obj = pool_lang.browse(cr, uid,pool_lang.search(cr, uid,[('code','=',lang)])[0])
158 # if os.name == 'nt':
159 # locale.setlocale(locale.LC_ALL, _LOCALE2WIN32.get(lang, lang) + '.' + encoding)
161 # locale.setlocale(locale.LC_ALL,str( lang + '.' + encoding))
163 # netsvc.Logger().notifyChannel('report', netsvc.LOG_WARNING,
164 # 'report %s: unable to set locale "%s"' % (self.name,
165 # self.object._context.get('lang', 'en_US') or 'en_US'))
168 class _float_format(float, _format):
171 if self._field and hasattr(self._field, 'digits') and self._field.digits:
172 digits = self._field.digits[1]
173 return self.lang_obj.format('%.' + str(digits) + 'f', self.name, True)
175 # if not self.object._context:
176 # return locale.format('%f', self.name, True)
178 # if hasattr(self._field, 'digits') and self._field.digits:
179 # digit = self._field.digits[1]
180 # return locale.format('%.' + str(digit) + 'f', self.name, True)
183 class _int_format(int, _format):
185 return self.lang_obj.format('%.d', self.name, True)
186 # return locale.format('%d', self.name, True)
189 class _date_format(str, _format):
192 date = mx.DateTime.strptime(self.name,DT_FORMAT)
193 return date.strftime(self.lang_obj.date_format)
195 # if not self.object._context:
200 # datedata = time.strptime(self.name, DT_FORMAT)
201 # return time.strftime(locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y'),
207 class _dttime_format(str, _format):
210 datetime = mx.DateTime.strptime(self.name,DHM_FORMAT)
211 return datetime.strftime(self.lang_obj.date_format+ " " + self.lang_obj.time_format)
216 'float': _float_format,
217 'date': _date_format,
218 'integer': _int_format,
219 'datetime' : _dttime_format
223 # Context: {'node': node.dom}
225 class browse_record_list(list):
226 def __init__(self, lst, context):
227 super(browse_record_list, self).__init__(lst)
228 self.context = context
230 def __getattr__(self, name):
231 res = browse_record_list([getattr(x,name) for x in self], self.context)
235 return "browse_record_list("+str(len(self))+")"
237 def repeatIn(self, name):
238 warnings.warn('Use repeatIn(object_list, \'variable\')', DeprecationWarning)
239 node = self.context['_node']
240 parents = self.context.get('parents', rml_parents)
243 if not node.parentNode:
245 node = node.parentNode
246 if node.nodeType == node.ELEMENT_NODE and node.localName in parents:
252 for i in range(1,len(self)):
253 newnode = parent_node.cloneNode(1)
254 n = parent_node.parentNode
255 n.insertBefore(newnode, parent_node)
256 nodes.append((i,newnode))
258 self.context[name] = self[i]
259 self.context['_self']._parse_node(node)
262 class rml_parse(object):
263 def __init__(self, cr, uid, name, parents=rml_parents, tag=rml_tag, context=None):
268 self.pool = pooler.get_pool(cr.dbname)
269 user = self.pool.get('res.users').browse(cr, uid, uid, fields_process=_fields_process)
270 self.localcontext = {
272 'company': user.company_id,
273 'repeatIn': self.repeatIn,
274 'setLang': self.setLang,
275 'setTag': self.setTag,
276 'removeParentNode': self.removeParentNode,
277 'format': self.format,
278 'formatLang': self.formatLang,
279 'logo' : user.company_id.logo,
280 'lang' : user.company_id.partner_id.lang,
282 self.localcontext.update(context)
283 self.rml_header = user.company_id.rml_header
284 self.rml_header2 = user.company_id.rml_header2
285 self.logo = user.company_id.logo
287 self._regex = re.compile('\[\[(.+?)\]\]')
288 self._transl_regex = re.compile('(\[\[.+?\]\])')
290 self.parents = parents
292 self._lang_cache = {}
295 def setTag(self, oldtag, newtag, attrs=None):
298 node = self._find_parent(self._node, [oldtag])
300 node.tagName = newtag
301 for key, val in attrs.items():
302 node.setAttribute(key, val)
305 def format(self, text, oldtag=None):
309 node = self._find_parent(self._node, [oldtag])
313 ns = node.nextSibling
317 lst = tools.ustr(text).split('\n')
318 if not (text and lst):
321 for i in range(len(lst)):
322 newnode = node.cloneNode(1)
323 newnode.tagName=rml_tag
324 newnode.childNodes[0].data = lst[i]
326 pp.insertBefore(newnode, ns)
328 pp.appendChild(newnode)
329 nodes.append((i, newnode))
331 def removeParentNode(self, tag=None):
334 if self.tag == sxw_tag and rml2sxw.get(tag, False):
336 node = self._find_parent(self._node, [tag])
338 parentNode = node.parentNode
339 parentNode.removeChild(node)
340 self._node = parentNode
342 def setLang(self, lang):
343 self.localcontext['lang'] = lang
344 for obj in self.objects:
345 obj._context['lang'] = lang
346 for table in obj._cache:
347 for id in obj._cache[table]:
348 self._lang_cache.setdefault(obj._context['lang'], {}).setdefault(table,
349 {}).update(obj._cache[table][id])
350 if lang in self._lang_cache \
351 and table in self._lang_cache[lang] \
352 and id in self._lang_cache[lang][table]:
353 obj._cache[table][id] = self._lang_cache[lang][table][id]
355 obj._cache[table][id] = {'id': id}
358 def formatLang(self, value, digits=2, date=False,date_time=False, grouping=True, monetary=False, currency=None):
359 if isinstance(value, (str, unicode)) and not value:
361 pool_lang = self.pool.get('res.lang')
362 lang = self.localcontext.get('lang', 'en_US') or 'en_US'
363 lang_obj = pool_lang.browse(self.cr,self.uid,pool_lang.search(self.cr,self.uid,[('code','=',lang)])[0])
364 if date or date_time:
365 date_format = lang_obj.date_format
367 date_format = lang_obj.date_format + " " + lang_obj.time_format
368 parse_format = date_format
370 # filtering time.strftime('%Y-%m-%d')
371 if type(value) == type(''):
372 parse_format = DHM_FORMAT
376 if not isinstance(value, time.struct_time):
377 # assume string, parse it
378 # if len(str(value)) == 10:
379 # # length of date like 2001-01-01 is ten
380 # # assume format '%Y-%m-%d'
381 # date = mx.DateTime.strptime(value,DT_FORMAT)
383 # # assume format '%Y-%m-%d %H:%M:%S'
384 # value = str(value)[:19]
385 # date = mx.DateTime.strptime(str(value),DHM_FORMAT)
386 date = mx.DateTime.strptime(str(value),parse_format)
388 date = mx.DateTime.DateTime(*(value.timetuple()[:6]))
389 return date.strftime(date_format)
390 return lang_obj.format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
392 def repeatIn(self, lst, name, nodes_parent=False):
394 node = self._find_parent(self._node, nodes_parent or self.parents)
397 ns = node.nextSibling
404 for i in range(len(lst)):
405 newnode = node.cloneNode(1)
407 pp.insertBefore(newnode, ns)
409 pp.appendChild(newnode)
410 nodes.append((i, newnode))
411 for i, node in nodes:
412 self.node_context[node] = {name: lst[i]}
415 def _eval(self, expr):
417 res = eval(expr, self.localcontext)
418 if (res is None) or (res=='') or (res is False):
421 import traceback, sys
422 tb_s = reduce(lambda x, y: x+y, traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
423 netsvc.Logger().notifyChannel('report', netsvc.LOG_ERROR,
424 'report %s:\n%s\n%s\nexpr: %s' % (self.name, tb_s, str(e),
425 expr.encode('utf-8')))
429 def _find_parent(self, node, parents):
431 if not node.parentNode:
433 node = node.parentNode
434 if node.nodeType == node.ELEMENT_NODE and node.localName in parents:
438 def _parse_text(self, text, level=None):
441 res = self._regex.findall(text)
444 # the "split [[]] if not match [[]]" is not very nice, but I
445 # don't see how I could do it better...
446 # what I'd like to do is a re.sub(NOT pattern, func, string)
447 # but I don't know how to do that...
448 # translate the RML file
449 if 'lang' in self.localcontext:
450 lang = self.localcontext['lang']
451 if lang and text and not text.isspace():
452 transl_obj = self.pool.get('ir.translation')
453 piece_list = self._transl_regex.split(text)
454 for pn in range(len(piece_list)):
455 if not self._transl_regex.match(piece_list[pn]):
456 source_string = piece_list[pn].replace('\n', ' ').strip()
457 if len(source_string):
458 translated_string = transl_obj._get_source(self.cr, self.uid, self.name, 'rml', lang, source_string)
459 if translated_string:
460 piece_list[pn] = piece_list[pn].replace(source_string, translated_string)
461 text = ''.join(piece_list)
463 newtext = self._eval(key)
464 for i in range(len(level)):
465 if isinstance(newtext, list):
466 newtext = newtext[level[i]]
467 if isinstance(newtext, list):
468 todo.append((key, newtext))
470 # if there are two [[]] blocks the same, it will replace both
471 # but it's ok because it should evaluate to the same thing
473 newtext = tools.ustr(newtext)
474 text = text.replace('[['+key+']]', newtext)
475 self._node.data = text
477 for key, newtext in todo:
478 parent_node = self._find_parent(self._node, parents)
479 assert parents.get(parent_node.localName, False), 'No parent node found !'
480 nodes = [parent_node]
481 for i in range(len(newtext) - 1):
482 newnode = parent_node.cloneNode(1)
483 if parents.get(parent_node.localName, False):
484 n = parent_node.parentNode
485 parent_node.parentNode.insertAfter(newnode, parent_node)
486 nodes.append(newnode)
490 def _parse_node(self):
493 if self._node.nodeType==self._node.ELEMENT_NODE:
494 if self._node.hasAttribute('expr'):
495 newattrs = self._eval(self._node.getAttribute('expr'))
496 for key,val in newattrs.items():
497 self._node.setAttribute(key,val)
499 if self._node.hasChildNodes():
500 self._node = self._node.firstChild
501 elif self._node.nextSibling:
502 self._node = self._node.nextSibling
504 while self._node and not self._node.nextSibling:
505 self._node = self._node.parentNode
508 self._node = self._node.nextSibling
509 if self._node in self.node_context:
510 self.localcontext.update(self.node_context[self._node])
511 if self._node.nodeType in (self._node.CDATA_SECTION_NODE, self._node.TEXT_NODE):
512 # if self._node in self.already:
513 # self.already[self._node] += 1
514 # print "second pass!", self.already[self._node], '---%s---' % self._node.data
516 # self.already[self._node] = 0
517 self._parse_text(self._node.data, level)
520 def _find_node(self, node, localname):
521 if node.localName==localname:
523 for tag in node.childNodes:
524 if tag.nodeType==tag.ELEMENT_NODE:
525 found = self._find_node(tag, localname)
530 def _add_header(self, node, header=1):
532 rml_head = self.rml_header2
534 rml_head = self.rml_header
536 # Refactor this patch, to use the minidom interface
537 if self.logo and (rml_head.find('company.logo')<0 or rml_head.find('<image')<0) and rml_head.find('<!--image')<0:
538 rml_head = rml_head.replace('<pageGraphics>','''<pageGraphics> <image x="10" y="26cm" height="70" width="90" >[[company.logo]] </image> ''')
539 if not self.logo and rml_head.find('company.logo')>=0:
540 rml_head = rml_head.replace('<image','<!--image')
541 rml_head = rml_head.replace('</image>','</image-->')
543 head_dom = xml.dom.minidom.parseString(rml_head)
544 #for frame in head_dom.getElementsByTagName('frame'):
545 # frame.parentNode.removeChild(frame)
546 node2 = head_dom.documentElement
547 for tag in node2.childNodes:
548 if tag.nodeType==tag.ELEMENT_NODE:
549 found = self._find_node(node, tag.localName)
550 # rml_frames = found.getElementsByTagName('frame')
552 if tag.hasAttribute('position') and (tag.getAttribute('position')=='inside'):
553 found.appendChild(tag)
555 found.parentNode.replaceChild(tag, found)
556 # for frame in rml_frames:
557 # tag.appendChild(frame)
560 def preprocess(self, objects, data, ids):
561 self.localcontext['data'] = data
562 self.localcontext['objects'] = objects
565 self.objects = objects
567 def _parse(self, rml_dom, objects, data, header=0):
568 self.node_context = {}
570 self._node = self.dom.documentElement
572 self._add_header(self._node, header)
574 res = self.dom.documentElement.toxml('utf-8')
577 class report_sxw(report_rml):
578 def __init__(self, name, table, rml, parser=rml_parse, header=True, store=False):
579 report_rml.__init__(self, name, table, rml, '')
585 def getObjects(self, cr, uid, ids, context):
586 table_obj = pooler.get_pool(cr.dbname).get(self.table)
587 return table_obj.browse(cr, uid, ids, list_class=browse_record_list, context=context,
588 fields_process=_fields_process)
590 def create(self, cr, uid, ids, data, context=None):
593 pool = pooler.get_pool(cr.dbname)
594 ir_obj = pool.get('ir.actions.report.xml')
595 report_xml_ids = ir_obj.search(cr, uid,
596 [('report_name', '=', self.name[7:])], context=context)
599 report_xml = ir_obj.browse(cr, uid, report_xml_ids[0],
601 attach = report_xml.attachment
603 ir_menu_report_obj = pool.get('ir.ui.menu')
604 report_menu_ids = ir_menu_report_obj.search(cr, uid,
605 [('id', 'in', ids)], context=context)
608 report_name = ir_menu_report_obj.browse(cr, uid, report_menu_ids[0],
610 title = report_name.name
611 rml = tools.file_open(self.tmpl, subdir=None).read()
612 report_type= data.get('report_type', 'pdf')
614 def __init__(self, *args, **argv):
615 for key,arg in argv.items():
616 setattr(self, key, arg)
617 report_xml = a(title=title, report_type=report_type, report_rml_content=rml, name=title, attachment=False, header=self.header)
621 objs = self.getObjects(cr, uid, ids, context)
624 aname = eval(attach, {'object':obj, 'time':time})
626 if report_xml.attachment_use and aname and context.get('attachment_use', True):
627 aids = pool.get('ir.attachment').search(cr, uid, [('datas_fname','=',aname+'.pdf'),('res_model','=',self.table),('res_id','=',obj.id)])
629 d = base64.decodestring(pool.get('ir.attachment').browse(cr, uid, aids[0]).datas)
630 results.append((d,'pdf'))
633 result = self.create_single(cr, uid, [obj.id], data, report_xml, context)
635 name = aname+'.'+result[1]
636 pool.get('ir.attachment').create(cr, uid, {
638 'datas': base64.encodestring(result[0]),
640 'res_model': self.table,
645 results.append(result)
647 if results[0][1]=='pdf':
648 from pyPdf import PdfFileWriter, PdfFileReader
650 output = PdfFileWriter()
652 reader = PdfFileReader(cStringIO.StringIO(r[0]))
653 for page in range(reader.getNumPages()):
654 output.addPage(reader.getPage(page))
655 s = cStringIO.StringIO()
657 return s.getvalue(), results[0][1]
658 return self.create_single(cr, uid, ids, data, report_xml, context)
660 def create_single(self, cr, uid, ids, data, report_xml, context={}):
662 context = context.copy()
663 pool = pooler.get_pool(cr.dbname)
664 want_header = self.header
665 title = report_xml.name
666 attach = report_xml.attachment
667 report_type = report_xml.report_type
668 want_header = report_xml.header
670 if report_type in ['sxw','odt']:
671 context['parents'] = sxw_parents
672 sxw_io = StringIO.StringIO(report_xml.report_sxw_content)
673 sxw_z = zipfile.ZipFile(sxw_io, mode='r')
674 rml = sxw_z.read('content.xml')
675 meta = sxw_z.read('meta.xml')
677 rml_parser = self.parser(cr, uid, self.name2, context)
678 rml_parser.parents = sxw_parents
679 rml_parser.tag = sxw_tag
680 objs = self.getObjects(cr, uid, ids, context)
681 rml_parser.preprocess(objs, data, ids)
682 rml_dom = xml.dom.minidom.parseString(rml)
684 node = rml_dom.documentElement
686 elements = node.getElementsByTagName("text:p")
689 e = pe.getElementsByTagName("text:drop-down")
692 for cnd in de.childNodes:
693 if cnd.nodeType in (cnd.CDATA_SECTION_NODE, cnd.TEXT_NODE):
697 # Add Information : Resource ID and Model
698 rml_dom_meta = xml.dom.minidom.parseString(meta)
699 node = rml_dom_meta.documentElement
700 elements = node.getElementsByTagName("meta:user-defined")
702 if pe.hasAttribute("meta:name"):
703 if pe.getAttribute("meta:name") == "Info 3":
704 pe.childNodes[0].data=data['id']
705 if pe.getAttribute("meta:name") == "Info 4":
706 pe.childNodes[0].data=data['model']
707 meta = rml_dom_meta.documentElement.toxml('utf-8')
709 rml2 = rml_parser._parse(rml_dom, objs, data, header=want_header)
710 sxw_z = zipfile.ZipFile(sxw_io, mode='a')
711 sxw_z.writestr('content.xml', "<?xml version='1.0' encoding='UTF-8'?>" + \
713 sxw_z.writestr('meta.xml', "<?xml version='1.0' encoding='UTF-8'?>" + \
717 #Add corporate header/footer
718 if report_type=='odt':
719 rml = tools.file_open('custom/corporate_odt_header.xml').read()
720 if report_type=='sxw':
721 rml = tools.file_open('custom/corporate_sxw_header.xml').read()
722 rml_parser = self.parser(cr, uid, self.name2, context)
723 rml_parser.parents = sxw_parents
724 rml_parser.tag = sxw_tag
725 objs = self.getObjects(cr, uid, ids, context)
726 rml_parser.preprocess(objs, data, ids)
727 rml_dom = xml.dom.minidom.parseString(rml)
728 rml2 = rml_parser._parse(rml_dom, objs, data, header=want_header)
729 sxw_z.writestr('styles.xml',"<?xml version='1.0' encoding='UTF-8'?>" + \
732 rml2 = sxw_io.getvalue()
735 rml = report_xml.report_rml_content
736 context['parents'] = rml_parents
737 rml_parser = self.parser(cr, uid, self.name2, context)
738 rml_parser.parents = rml_parents
739 rml_parser.tag = rml_tag
740 objs = self.getObjects(cr, uid, ids, context)
741 rml_parser.preprocess(objs, data, ids)
742 rml_dom = xml.dom.minidom.parseString(rml)
743 rml2 = rml_parser._parse(rml_dom, objs, data, header=want_header)
745 logo = base64.decodestring(rml_parser.logo)
747 create_doc = self.generators[report_type]
748 pdf = create_doc(rml2, logo, title.encode('utf8'))
750 return (pdf, report_type)