[IMP] tools: mail: improved html_email_clean
[odoo/odoo.git] / openerp / tools / convert.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import cStringIO
23 import csv
24 import logging
25 import os.path
26 import pickle
27 import re
28 import sys
29
30 # for eval context:
31 import time
32
33 import openerp
34 import openerp.release
35 import openerp.workflow
36
37 import assertion_report
38
39 _logger = logging.getLogger(__name__)
40
41 try:
42     import pytz
43 except:
44     _logger.warning('could not find pytz library, please install it')
45     class pytzclass(object):
46         all_timezones=[]
47     pytz=pytzclass()
48
49
50 from datetime import datetime, timedelta
51 from dateutil.relativedelta import relativedelta
52 from lxml import etree
53 import misc
54 from config import config
55 from translate import _
56
57 # List of etree._Element subclasses that we choose to ignore when parsing XML.
58 from misc import SKIPPED_ELEMENT_TYPES
59
60 from misc import unquote
61
62 # Import of XML records requires the unsafe eval as well,
63 # almost everywhere, which is ok because it supposedly comes
64 # from trusted data, but at least we make it obvious now.
65 unsafe_eval = eval
66 from safe_eval import safe_eval as eval
67
68 class ParseError(Exception):
69     def __init__(self, msg, text, filename, lineno):
70         self.msg = msg
71         self.text = text
72         self.filename = filename
73         self.lineno = lineno
74
75     def __str__(self):
76         return '"%s" while parsing %s:%s, near\n%s' \
77             % (self.msg, self.filename, self.lineno, self.text)
78
79 def _ref(self, cr):
80     return lambda x: self.id_get(cr, x)
81
82 def _obj(pool, cr, uid, model_str, context=None):
83     model = pool[model_str]
84     return lambda x: model.browse(cr, uid, x, context=context)
85
86 def _get_idref(self, cr, uid, model_str, context, idref):
87     idref2 = dict(idref,
88                   time=time,
89                   DateTime=datetime,
90                   datetime=datetime,
91                   timedelta=timedelta,
92                   relativedelta=relativedelta,
93                   version=openerp.release.major_version,
94                   ref=_ref(self, cr),
95                   pytz=pytz)
96     if len(model_str):
97         idref2['obj'] = _obj(self.pool, cr, uid, model_str, context=context)
98     return idref2
99
100 def _fix_multiple_roots(node):
101     """
102     Surround the children of the ``node`` element of an XML field with a
103     single root "data" element, to prevent having a document with multiple
104     roots once parsed separately.
105
106     XML nodes should have one root only, but we'd like to support
107     direct multiple roots in our partial documents (like inherited view architectures).
108     As a convention we'll surround multiple root with a container "data" element, to be
109     ignored later when parsing.
110     """
111     real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
112     if len(real_nodes) > 1:
113         data_node = etree.Element("data")
114         for child in node:
115             data_node.append(child)
116         node.append(data_node)
117
118 def _eval_xml(self, node, pool, cr, uid, idref, context=None):
119     if context is None:
120         context = {}
121     if node.tag in ('field','value'):
122         t = node.get('type','char')
123         f_model = node.get('model', '').encode('utf-8')
124         if node.get('search'):
125             f_search = node.get("search",'').encode('utf-8')
126             f_use = node.get("use",'id').encode('utf-8')
127             f_name = node.get("name",'').encode('utf-8')
128             idref2 = {}
129             if f_search:
130                 idref2 = _get_idref(self, cr, uid, f_model, context, idref)
131             q = unsafe_eval(f_search, idref2)
132             ids = pool[f_model].search(cr, uid, q)
133             if f_use != 'id':
134                 ids = map(lambda x: x[f_use], pool[f_model].read(cr, uid, ids, [f_use]))
135             _cols = pool[f_model]._columns
136             if (f_name in _cols) and _cols[f_name]._type=='many2many':
137                 return ids
138             f_val = False
139             if len(ids):
140                 f_val = ids[0]
141                 if isinstance(f_val, tuple):
142                     f_val = f_val[0]
143             return f_val
144         a_eval = node.get('eval','')
145         if a_eval:
146             idref2 = _get_idref(self, cr, uid, f_model, context, idref)
147             try:
148                 return unsafe_eval(a_eval, idref2)
149             except Exception:
150                 logging.getLogger('openerp.tools.convert.init').error(
151                     'Could not eval(%s) for %s in %s', a_eval, node.get('name'), context)
152                 raise
153         def _process(s, idref):
154             matches = re.finditer('[^%]%\((.*?)\)[ds]', s)
155             done = []
156             for m in matches:
157                 found = m.group()[1:]
158                 if found in done:
159                     continue
160                 done.append(found)
161                 id = m.groups()[0]
162                 if not id in idref:
163                     idref[id] = self.id_get(cr, id)
164                 s = s.replace(found, str(idref[id]))
165
166             s = s.replace('%%', '%') # Quite wierd but it's for (somewhat) backward compatibility sake
167
168             return s
169
170         if t == 'xml':
171             _fix_multiple_roots(node)
172             return '<?xml version="1.0"?>\n'\
173                 +_process("".join([etree.tostring(n, encoding='utf-8')
174                                    for n in node]), idref)
175         if t == 'html':
176             return _process("".join([etree.tostring(n, encoding='utf-8')
177                                    for n in node]), idref)
178         if node.get('file'):
179             import openerp.tools
180             import base64
181             fp = openerp.tools.file_open(node.get('file'))
182             result = base64.b64encode(fp.read())
183             return result
184
185         if t == 'file':
186             from ..modules import module
187             path = node.text.strip()
188             if not module.get_module_resource(self.module, path):
189                 raise IOError("No such file or directory: '%s' in %s" % (
190                     path, self.module))
191             return '%s,%s' % (self.module, path)
192         if t in ('char', 'int', 'float'):
193             d = node.text
194             if t == 'int':
195                 d = d.strip()
196                 if d == 'None':
197                     return None
198                 else:
199                     return int(d.strip())
200             elif t == 'float':
201                 return float(d.strip())
202             return d
203         elif t in ('list','tuple'):
204             res=[]
205             for n in node.findall('./value'):
206                 res.append(_eval_xml(self,n,pool,cr,uid,idref))
207             if t=='tuple':
208                 return tuple(res)
209             return res
210     elif node.tag == "getitem":
211         for n in node:
212             res=_eval_xml(self,n,pool,cr,uid,idref)
213         if not res:
214             raise LookupError
215         elif node.get('type') in ("int", "list"):
216             return res[int(node.get('index'))]
217         else:
218             return res[node.get('index','').encode("utf8")]
219     elif node.tag == "function":
220         args = []
221         a_eval = node.get('eval','')
222         if a_eval:
223             idref['ref'] = lambda x: self.id_get(cr, x)
224             args = unsafe_eval(a_eval, idref)
225         for n in node:
226             return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
227             if return_val is not None:
228                 args.append(return_val)
229         model = pool[node.get('model','')]
230         method = node.get('name','')
231         res = getattr(model, method)(cr, uid, *args)
232         return res
233     elif node.tag == "test":
234         return node.text
235
236 escape_re = re.compile(r'(?<!\\)/')
237 def escape(x):
238     return x.replace('\\/', '/')
239
240 class xml_import(object):
241     @staticmethod
242     def nodeattr2bool(node, attr, default=False):
243         if not node.get(attr):
244             return default
245         val = node.get(attr).strip()
246         if not val:
247             return default
248         return val.lower() not in ('0', 'false', 'off')
249
250     def isnoupdate(self, data_node=None):
251         return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
252
253     def get_context(self, data_node, node, eval_dict):
254         data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
255         node_context = node.get("context",'').encode('utf8')
256         context = {}
257         for ctx in (data_node_context, node_context):
258             if ctx:
259                 try:
260                     ctx_res = unsafe_eval(ctx, eval_dict)
261                     if isinstance(context, dict):
262                         context.update(ctx_res)
263                     else:
264                         context = ctx_res
265                 except NameError:
266                     # Some contexts contain references that are only valid at runtime at
267                     # client-side, so in that case we keep the original context string
268                     # as it is. We also log it, just in case.
269                     context = ctx
270                     _logger.debug('Context value (%s) for element with id "%s" or its data node does not parse '\
271                                                     'at server-side, keeping original string, in case it\'s meant for client side only',
272                                                     ctx, node.get('id','n/a'), exc_info=True)
273         return context
274
275     def get_uid(self, cr, uid, data_node, node):
276         node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
277         if node_uid:
278             return self.id_get(cr, node_uid)
279         return uid
280
281     def _test_xml_id(self, xml_id):
282         id = xml_id
283         if '.' in xml_id:
284             module, id = xml_id.split('.', 1)
285             assert '.' not in id, """The ID reference "%s" must contain
286 maximum one dot. They are used to refer to other modules ID, in the
287 form: module.record_id""" % (xml_id,)
288             if module != self.module:
289                 modcnt = self.pool['ir.module.module'].search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
290                 assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
291
292         if len(id) > 64:
293             _logger.error('id: %s is to long (max: 64)', id)
294
295     def _tag_delete(self, cr, rec, data_node=None):
296         d_model = rec.get("model",'')
297         d_search = rec.get("search",'').encode('utf-8')
298         d_id = rec.get("id",'')
299         ids = []
300
301         if d_search:
302             idref = _get_idref(self, cr, self.uid, d_model, context={}, idref={})
303             ids = self.pool[d_model].search(cr, self.uid, unsafe_eval(d_search, idref))
304         if d_id:
305             try:
306                 ids.append(self.id_get(cr, d_id))
307             except:
308                 # d_id cannot be found. doesn't matter in this case
309                 pass
310         if ids:
311             self.pool[d_model].unlink(cr, self.uid, ids)
312
313     def _remove_ir_values(self, cr, name, value, model):
314         ir_values_obj = self.pool['ir.values']
315         ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
316         if ir_value_ids:
317             ir_values_obj.unlink(cr, self.uid, ir_value_ids)
318
319         return True
320
321     def _tag_report(self, cr, rec, data_node=None):
322         res = {}
323         for dest,f in (('name','string'),('model','model'),('report_name','name')):
324             res[dest] = rec.get(f,'').encode('utf8')
325             assert res[dest], "Attribute %s of report is empty !" % (f,)
326         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
327                            ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage'),
328                            ('report_type', 'report_type'), ('parser', 'parser')):
329             if rec.get(field):
330                 res[dest] = rec.get(field).encode('utf8')
331         if rec.get('auto'):
332             res['auto'] = eval(rec.get('auto','False'))
333         if rec.get('sxw'):
334             sxw_content = misc.file_open(rec.get('sxw')).read()
335             res['report_sxw_content'] = sxw_content
336         if rec.get('header'):
337             res['header'] = eval(rec.get('header','False'))
338
339         res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
340
341         xml_id = rec.get('id','').encode('utf8')
342         self._test_xml_id(xml_id)
343
344         if rec.get('groups'):
345             g_names = rec.get('groups','').split(',')
346             groups_value = []
347             for group in g_names:
348                 if group.startswith('-'):
349                     group_id = self.id_get(cr, group[1:])
350                     groups_value.append((3, group_id))
351                 else:
352                     group_id = self.id_get(cr, group)
353                     groups_value.append((4, group_id))
354             res['groups_id'] = groups_value
355
356         id = self.pool['ir.model.data']._update(cr, self.uid, "ir.actions.report.xml", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
357         self.idref[xml_id] = int(id)
358
359         if not rec.get('menu') or eval(rec.get('menu','False')):
360             keyword = str(rec.get('keyword', 'client_print_multi'))
361             value = 'ir.actions.report.xml,'+str(id)
362             replace = rec.get('replace', True)
363             self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, res['name'], [res['model']], value, replace=replace, isobject=True, xml_id=xml_id)
364         elif self.mode=='update' and eval(rec.get('menu','False'))==False:
365             # Special check for report having attribute menu=False on update
366             value = 'ir.actions.report.xml,'+str(id)
367             self._remove_ir_values(cr, res['name'], value, res['model'])
368         return id
369
370     def _tag_function(self, cr, rec, data_node=None):
371         if self.isnoupdate(data_node) and self.mode != 'init':
372             return
373         context = self.get_context(data_node, rec, {'ref': _ref(self, cr)})
374         uid = self.get_uid(cr, self.uid, data_node, rec)
375         _eval_xml(self,rec, self.pool, cr, uid, self.idref, context=context)
376         return
377
378     def _tag_wizard(self, cr, rec, data_node=None):
379         string = rec.get("string",'').encode('utf8')
380         model = rec.get("model",'').encode('utf8')
381         name = rec.get("name",'').encode('utf8')
382         xml_id = rec.get('id','').encode('utf8')
383         self._test_xml_id(xml_id)
384         multi = rec.get('multi','') and eval(rec.get('multi','False'))
385         res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
386
387         if rec.get('groups'):
388             g_names = rec.get('groups','').split(',')
389             groups_value = []
390             for group in g_names:
391                 if group.startswith('-'):
392                     group_id = self.id_get(cr, group[1:])
393                     groups_value.append((3, group_id))
394                 else:
395                     group_id = self.id_get(cr, group)
396                     groups_value.append((4, group_id))
397             res['groups_id'] = groups_value
398
399         id = self.pool['ir.model.data']._update(cr, self.uid, "ir.actions.wizard", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
400         self.idref[xml_id] = int(id)
401         # ir_set
402         if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
403             keyword = str(rec.get('keyword','') or 'client_action_multi')
404             value = 'ir.actions.wizard,'+str(id)
405             replace = rec.get("replace",'') or True
406             self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
407         elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
408             # Special check for wizard having attribute menu=False on update
409             value = 'ir.actions.wizard,'+str(id)
410             self._remove_ir_values(cr, string, value, model)
411
412     def _tag_url(self, cr, rec, data_node=None):
413         url = rec.get("url",'').encode('utf8')
414         target = rec.get("target",'').encode('utf8')
415         name = rec.get("name",'').encode('utf8')
416         xml_id = rec.get('id','').encode('utf8')
417         self._test_xml_id(xml_id)
418
419         res = {'name': name, 'url': url, 'target':target}
420
421         id = self.pool['ir.model.data']._update(cr, self.uid, "ir.actions.act_url", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
422         self.idref[xml_id] = int(id)
423
424     def _tag_act_window(self, cr, rec, data_node=None):
425         name = rec.get('name','').encode('utf-8')
426         xml_id = rec.get('id','').encode('utf8')
427         self._test_xml_id(xml_id)
428         type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
429         view_id = False
430         if rec.get('view_id'):
431             view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
432         domain = rec.get('domain','').encode('utf-8') or '[]'
433         res_model = rec.get('res_model','').encode('utf-8')
434         src_model = rec.get('src_model','').encode('utf-8')
435         view_type = rec.get('view_type','').encode('utf-8') or 'form'
436         view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
437         usage = rec.get('usage','').encode('utf-8')
438         limit = rec.get('limit','').encode('utf-8')
439         auto_refresh = rec.get('auto_refresh','').encode('utf-8')
440         uid = self.uid
441
442         # Act_window's 'domain' and 'context' contain mostly literals
443         # but they can also refer to the variables provided below
444         # in eval_context, so we need to eval() them before storing.
445         # Among the context variables, 'active_id' refers to
446         # the currently selected items in a list view, and only
447         # takes meaning at runtime on the client side. For this
448         # reason it must remain a bare variable in domain and context,
449         # even after eval() at server-side. We use the special 'unquote'
450         # class to achieve this effect: a string which has itself, unquoted,
451         # as representation.
452         active_id = unquote("active_id")
453         active_ids = unquote("active_ids")
454         active_model = unquote("active_model")
455
456         def ref(str_id):
457             return self.id_get(cr, str_id)
458
459         # Include all locals() in eval_context, for backwards compatibility
460         eval_context = {
461             'name': name,
462             'xml_id': xml_id,
463             'type': type,
464             'view_id': view_id,
465             'domain': domain,
466             'res_model': res_model,
467             'src_model': src_model,
468             'view_type': view_type,
469             'view_mode': view_mode,
470             'usage': usage,
471             'limit': limit,
472             'auto_refresh': auto_refresh,
473             'uid' : uid,
474             'active_id': active_id,
475             'active_ids': active_ids,
476             'active_model': active_model,
477             'ref' : ref,
478         }
479         context = self.get_context(data_node, rec, eval_context)
480
481         try:
482             domain = unsafe_eval(domain, eval_context)
483         except NameError:
484             # Some domains contain references that are only valid at runtime at
485             # client-side, so in that case we keep the original domain string
486             # as it is. We also log it, just in case.
487             _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
488                 'at server-side, keeping original string, in case it\'s meant for client side only',
489                 domain, xml_id or 'n/a', exc_info=True)
490         res = {
491             'name': name,
492             'type': type,
493             'view_id': view_id,
494             'domain': domain,
495             'context': context,
496             'res_model': res_model,
497             'src_model': src_model,
498             'view_type': view_type,
499             'view_mode': view_mode,
500             'usage': usage,
501             'limit': limit,
502             'auto_refresh': auto_refresh,
503         }
504
505         if rec.get('groups'):
506             g_names = rec.get('groups','').split(',')
507             groups_value = []
508             for group in g_names:
509                 if group.startswith('-'):
510                     group_id = self.id_get(cr, group[1:])
511                     groups_value.append((3, group_id))
512                 else:
513                     group_id = self.id_get(cr, group)
514                     groups_value.append((4, group_id))
515             res['groups_id'] = groups_value
516
517         if rec.get('target'):
518             res['target'] = rec.get('target','')
519         if rec.get('multi'):
520             res['multi'] = eval(rec.get('multi', 'False'))
521         id = self.pool['ir.model.data']._update(cr, self.uid, 'ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
522         self.idref[xml_id] = int(id)
523
524         if src_model:
525             #keyword = 'client_action_relate'
526             keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
527             value = 'ir.actions.act_window,'+str(id)
528             replace = rec.get('replace','') or True
529             self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', keyword, xml_id, [src_model], value, replace=replace, isobject=True, xml_id=xml_id)
530         # TODO add remove ir.model.data
531
532     def _tag_ir_set(self, cr, rec, data_node=None):
533         if self.mode != 'init':
534             return
535         res = {}
536         for field in rec.findall('./field'):
537             f_name = field.get("name",'').encode('utf-8')
538             f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
539             res[f_name] = f_val
540         self.pool['ir.model.data'].ir_set(cr, self.uid, res['key'], res['key2'], res['name'], res['models'], res['value'], replace=res.get('replace',True), isobject=res.get('isobject', False), meta=res.get('meta',None))
541
542     def _tag_workflow(self, cr, rec, data_node=None):
543         if self.isnoupdate(data_node) and self.mode != 'init':
544             return
545         model = str(rec.get('model',''))
546         w_ref = rec.get('ref','')
547         if w_ref:
548             id = self.id_get(cr, w_ref)
549         else:
550             number_children = len(rec)
551             assert number_children > 0,\
552                 'You must define a child node if you dont give a ref'
553             assert number_children == 1,\
554                 'Only one child node is accepted (%d given)' % number_children
555             id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
556
557         uid = self.get_uid(cr, self.uid, data_node, rec)
558         openerp.workflow.trg_validate(uid, model,
559             id,
560             str(rec.get('action','')), cr)
561
562     #
563     # Support two types of notation:
564     #   name="Inventory Control/Sending Goods"
565     # or
566     #   action="action_id"
567     #   parent="parent_id"
568     #
569     def _tag_menuitem(self, cr, rec, data_node=None):
570         rec_id = rec.get("id",'').encode('ascii')
571         self._test_xml_id(rec_id)
572         m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
573
574         values = {'parent_id': False}
575         if rec.get('parent', False) is False and len(m_l) > 1:
576             # No parent attribute specified and the menu name has several menu components,
577             # try to determine the ID of the parent according to menu path
578             pid = False
579             res = None
580             values['name'] = m_l[-1]
581             m_l = m_l[:-1] # last part is our name, not a parent
582             for idx, menu_elem in enumerate(m_l):
583                 if pid:
584                     cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
585                 else:
586                     cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
587                 res = cr.fetchone()
588                 if res:
589                     pid = res[0]
590                 else:
591                     # the menuitem does't exist but we are in branch (not a leaf)
592                     _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
593                     pid = self.pool['ir.ui.menu'].create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
594             values['parent_id'] = pid
595         else:
596             # The parent attribute was specified, if non-empty determine its ID, otherwise
597             # explicitly make a top-level menu
598             if rec.get('parent'):
599                 menu_parent_id = self.id_get(cr, rec.get('parent',''))
600             else:
601                 # we get here with <menuitem parent="">, explicit clear of parent, or
602                 # if no parent attribute at all but menu name is not a menu path
603                 menu_parent_id = False
604             values = {'parent_id': menu_parent_id}
605             if rec.get('name'):
606                 values['name'] = rec.get('name')
607             try:
608                 res = [ self.id_get(cr, rec.get('id','')) ]
609             except:
610                 res = None
611
612         if rec.get('action'):
613             a_action = rec.get('action','').encode('utf8')
614
615             # determine the type of action
616             a_type, a_id = self.model_id_get(cr, a_action)
617             a_type = a_type.split('.')[-1] # keep only type part
618
619             icons = {
620                 "act_window": 'STOCK_NEW',
621                 "report.xml": 'STOCK_PASTE',
622                 "wizard": 'STOCK_EXECUTE',
623                 "url": 'STOCK_JUMP_TO',
624                 "client": 'STOCK_EXECUTE',
625                 "server": 'STOCK_EXECUTE',
626             }
627             values['icon'] = icons.get(a_type,'STOCK_NEW')
628
629             if a_type=='act_window':
630                 cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
631                 rrres = cr.fetchone()
632                 assert rrres, "No window action defined for this id %s !\n" \
633                     "Verify that this is a window action or add a type argument." % (a_action,)
634                 action_type,action_mode,action_name,view_id,target = rrres
635                 if view_id:
636                     view_arch = self.pool['ir.ui.view'].read(cr, 1, [view_id], ['arch'])
637                     action_mode = etree.fromstring(view_arch[0]['arch'].encode('utf8')).tag
638                 cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
639                 if cr.rowcount:
640                     action_mode, = cr.fetchone()
641                 if action_type=='tree':
642                     values['icon'] = 'STOCK_INDENT'
643                 elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
644                     values['icon'] = 'STOCK_JUSTIFY_FILL'
645                 elif action_mode and action_mode.startswith('graph'):
646                     values['icon'] = 'terp-graph'
647                 elif action_mode and action_mode.startswith('calendar'):
648                     values['icon'] = 'terp-calendar'
649                 if target=='new':
650                     values['icon'] = 'STOCK_EXECUTE'
651                 if not values.get('name', False):
652                     values['name'] = action_name
653
654             elif a_type in ['wizard', 'url', 'client', 'server'] and not values.get('name'):
655                 a_table = 'ir_act_%s' % a_type
656                 cr.execute('select name from %s where id=%%s' % a_table, (int(a_id),))
657                 resw = cr.fetchone()
658                 if resw:
659                     values['name'] = resw[0]
660
661         if not values.get('name'):
662             # ensure menu has a name
663             values['name'] = rec_id or '?'
664
665         if rec.get('sequence'):
666             values['sequence'] = int(rec.get('sequence'))
667         if rec.get('icon'):
668             values['icon'] = str(rec.get('icon'))
669         if rec.get('web_icon'):
670             values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
671         if rec.get('web_icon_hover'):
672             values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
673
674         if rec.get('groups'):
675             g_names = rec.get('groups','').split(',')
676             groups_value = []
677             for group in g_names:
678                 if group.startswith('-'):
679                     group_id = self.id_get(cr, group[1:])
680                     groups_value.append((3, group_id))
681                 else:
682                     group_id = self.id_get(cr, group)
683                     groups_value.append((4, group_id))
684             values['groups_id'] = groups_value
685
686         pid = self.pool['ir.model.data']._update(cr, self.uid, 'ir.ui.menu', self.module, values, rec_id, noupdate=self.isnoupdate(data_node), mode=self.mode, res_id=res and res[0] or False)
687
688         if rec_id and pid:
689             self.idref[rec_id] = int(pid)
690
691         if rec.get('action') and pid:
692             action = "ir.actions.%s,%d" % (a_type, a_id)
693             self.pool['ir.model.data'].ir_set(cr, self.uid, 'action', 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(pid))], action, True, True, xml_id=rec_id)
694         return 'ir.ui.menu', pid
695
696     def _assert_equals(self, f1, f2, prec=4):
697         return not round(f1 - f2, prec)
698
699     def _tag_assert(self, cr, rec, data_node=None):
700         if self.isnoupdate(data_node) and self.mode != 'init':
701             return
702
703         rec_model = rec.get("model",'').encode('ascii')
704         model = self.pool[rec_model]
705         rec_id = rec.get("id",'').encode('ascii')
706         self._test_xml_id(rec_id)
707         rec_src = rec.get("search",'').encode('utf8')
708         rec_src_count = rec.get("count")
709
710         rec_string = rec.get("string",'').encode('utf8') or 'unknown'
711
712         ids = None
713         eval_dict = {'ref': _ref(self, cr)}
714         context = self.get_context(data_node, rec, eval_dict)
715         uid = self.get_uid(cr, self.uid, data_node, rec)
716         if rec_id:
717             ids = [self.id_get(cr, rec_id)]
718         elif rec_src:
719             q = unsafe_eval(rec_src, eval_dict)
720             ids = self.pool[rec_model].search(cr, uid, q, context=context)
721             if rec_src_count:
722                 count = int(rec_src_count)
723                 if len(ids) != count:
724                     self.assertion_report.record_failure()
725                     msg = 'assertion "%s" failed!\n'    \
726                           ' Incorrect search count:\n'  \
727                           ' expected count: %d\n'       \
728                           ' obtained count: %d\n'       \
729                           % (rec_string, count, len(ids))
730                     _logger.error(msg)
731                     return
732
733         assert ids is not None,\
734             'You must give either an id or a search criteria'
735         ref = _ref(self, cr)
736         for id in ids:
737             brrec =  model.browse(cr, uid, id, context)
738             class d(dict):
739                 def __getitem__(self2, key):
740                     if key in brrec:
741                         return brrec[key]
742                     return dict.__getitem__(self2, key)
743             globals_dict = d()
744             globals_dict['floatEqual'] = self._assert_equals
745             globals_dict['ref'] = ref
746             globals_dict['_ref'] = ref
747             for test in rec.findall('./test'):
748                 f_expr = test.get("expr",'').encode('utf-8')
749                 expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
750                 expression_value = unsafe_eval(f_expr, globals_dict)
751                 if expression_value != expected_value: # assertion failed
752                     self.assertion_report.record_failure()
753                     msg = 'assertion "%s" failed!\n'    \
754                           ' xmltag: %s\n'               \
755                           ' expected value: %r\n'       \
756                           ' obtained value: %r\n'       \
757                           % (rec_string, etree.tostring(test), expected_value, expression_value)
758                     _logger.error(msg)
759                     return
760         else: # all tests were successful for this assertion tag (no break)
761             self.assertion_report.record_success()
762
763     def _tag_record(self, cr, rec, data_node=None):
764         rec_model = rec.get("model").encode('ascii')
765         model = self.pool[rec_model]
766         rec_id = rec.get("id",'').encode('ascii')
767         rec_context = rec.get("context", None)
768         if rec_context:
769             rec_context = unsafe_eval(rec_context)
770         self._test_xml_id(rec_id)
771         if self.isnoupdate(data_node) and self.mode != 'init':
772             # check if the xml record has an id string
773             if rec_id:
774                 if '.' in rec_id:
775                     module,rec_id2 = rec_id.split('.')
776                 else:
777                     module = self.module
778                     rec_id2 = rec_id
779                 id = self.pool['ir.model.data']._update_dummy(cr, self.uid, rec_model, module, rec_id2)
780                 # check if the resource already existed at the last update
781                 if id:
782                     # if it existed, we don't update the data, but we need to
783                     # know the id of the existing record anyway
784                     self.idref[rec_id] = int(id)
785                     return None
786                 else:
787                     # if the resource didn't exist
788                     if not self.nodeattr2bool(rec, 'forcecreate', True):
789                         # we don't want to create it, so we skip it
790                         return None
791                     # else, we let the record to be created
792
793             else:
794                 # otherwise it is skipped
795                 return None
796         res = {}
797         for field in rec.findall('./field'):
798 #TODO: most of this code is duplicated above (in _eval_xml)...
799             f_name = field.get("name",'').encode('utf-8')
800             f_ref = field.get("ref",'').encode('utf-8')
801             f_search = field.get("search",'').encode('utf-8')
802             f_model = field.get("model",'').encode('utf-8')
803             if not f_model and model._columns.get(f_name,False):
804                 f_model = model._columns[f_name]._obj
805             f_use = field.get("use",'').encode('utf-8') or 'id'
806             f_val = False
807
808             if f_search:
809                 q = unsafe_eval(f_search, self.idref)
810                 field = []
811                 assert f_model, 'Define an attribute model="..." in your .XML file !'
812                 f_obj = self.pool[f_model]
813                 # browse the objects searched
814                 s = f_obj.browse(cr, self.uid, f_obj.search(cr, self.uid, q))
815                 # column definitions of the "local" object
816                 _cols = self.pool[rec_model]._columns
817                 # if the current field is many2many
818                 if (f_name in _cols) and _cols[f_name]._type=='many2many':
819                     f_val = [(6, 0, map(lambda x: x[f_use], s))]
820                 elif len(s):
821                     # otherwise (we are probably in a many2one field),
822                     # take the first element of the search
823                     f_val = s[0][f_use]
824             elif f_ref:
825                 if f_ref=="null":
826                     f_val = False
827                 else:
828                     if f_name in model._columns \
829                               and model._columns[f_name]._type == 'reference':
830                         val = self.model_id_get(cr, f_ref)
831                         f_val = val[0] + ',' + str(val[1])
832                     else:
833                         f_val = self.id_get(cr, f_ref)
834             else:
835                 f_val = _eval_xml(self,field, self.pool, cr, self.uid, self.idref)
836                 if model._columns.has_key(f_name):
837                     import openerp.osv as osv
838                     if isinstance(model._columns[f_name], osv.fields.integer):
839                         f_val = int(f_val)
840             res[f_name] = f_val
841
842         id = self.pool['ir.model.data']._update(cr, self.uid, rec_model, self.module, res, rec_id or False, not self.isnoupdate(data_node), noupdate=self.isnoupdate(data_node), mode=self.mode, context=rec_context )
843         if rec_id:
844             self.idref[rec_id] = int(id)
845         if config.get('import_partial', False):
846             cr.commit()
847         return rec_model, id
848
849     def _tag_template(self, cr, el, data_node=None):
850         # This helper transforms a <template> element into a <record> and forwards it
851         tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
852         module = self.module
853         if '.' in tpl_id:
854             module, tpl_id = tpl_id.split('.', 1)
855         # set the full template name for qweb <module>.<id>
856         if not (el.get('inherit_id') or el.get('inherit_option_id')):
857             el.attrib['t-name'] = '%s.%s' % (module, tpl_id)
858             el.tag = 't'
859         else:
860             el.tag = 'data'
861         el.attrib.pop('id', None)
862
863         record = etree.Element('record')
864         record_attrs = {
865             'id': tpl_id,
866             'model': 'ir.ui.view',
867         }
868         for att in ['forcecreate', 'context']:
869             if att in el.attrib:
870                 record_attrs[att] = el.attrib.pop(att)
871
872         record.attrib.update(record_attrs)
873         name = el.get('name', tpl_id)
874         record.append(etree.fromstring('<field name="name">%s</field>' % name))
875         record.append(etree.fromstring('<field name="type">qweb</field>'))
876         record.append(etree.fromstring('<field name="arch" type="xml"/>'))
877         record[-1].append(el)
878         for key in ('inherit_id','inherit_option_id'):
879             if el.get(key):
880                 record.append(etree.fromstring('<field name="%s" ref="%s"/>' % (key, el.get(key))))
881                 el.attrib.pop(key, None)
882         if el.get('page'):
883             record.append(etree.Element('field', name="page", eval="True"))
884         return self._tag_record(cr, record, data_node)
885
886     def id_get(self, cr, id_str):
887         if id_str in self.idref:
888             return self.idref[id_str]
889         res = self.model_id_get(cr, id_str)
890         if res and len(res)>1: res = res[1]
891         return res
892
893     def model_id_get(self, cr, id_str):
894         model_data_obj = self.pool['ir.model.data']
895         mod = self.module
896         if '.' in id_str:
897             mod,id_str = id_str.split('.')
898         return model_data_obj.get_object_reference(cr, self.uid, mod, id_str)
899
900     def parse(self, de):
901         if de.tag != 'openerp':
902             raise Exception("Mismatch xml format: root tag must be `openerp`.")
903
904         for n in de.findall('./data'):
905             for rec in n:
906                 if rec.tag in self._tags:
907                     try:
908                         self._tags[rec.tag](self.cr, rec, n)
909                     except Exception, e:
910                         self.cr.rollback()
911                         exc_info = sys.exc_info()
912                         raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
913         return True
914
915     def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
916
917         self.mode = mode
918         self.module = module
919         self.cr = cr
920         self.idref = idref
921         self.pool = openerp.registry(cr.dbname)
922         self.uid = 1
923         if report is None:
924             report = assertion_report.assertion_report()
925         self.assertion_report = report
926         self.noupdate = noupdate
927         self._tags = {
928             'menuitem': self._tag_menuitem,
929             'record': self._tag_record,
930             'template': self._tag_template,
931             'assert': self._tag_assert,
932             'report': self._tag_report,
933             'wizard': self._tag_wizard,
934             'delete': self._tag_delete,
935             'ir_set': self._tag_ir_set,
936             'function': self._tag_function,
937             'workflow': self._tag_workflow,
938             'act_window': self._tag_act_window,
939             'url': self._tag_url
940         }
941
942 def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
943         noupdate=False):
944     '''Import csv file :
945         quote: "
946         delimiter: ,
947         encoding: utf-8'''
948     if not idref:
949         idref={}
950     model = ('.'.join(fname.split('.')[:-1]).split('-'))[0]
951     #remove folder path from model
952     head, model = os.path.split(model)
953
954     input = cStringIO.StringIO(csvcontent) #FIXME
955     reader = csv.reader(input, quotechar='"', delimiter=',')
956     fields = reader.next()
957     fname_partial = ""
958     if config.get('import_partial'):
959         fname_partial = module + '/'+ fname
960         if not os.path.isfile(config.get('import_partial')):
961             pickle.dump({}, file(config.get('import_partial'),'w+'))
962         else:
963             data = pickle.load(file(config.get('import_partial')))
964             if fname_partial in data:
965                 if not data[fname_partial]:
966                     return
967                 else:
968                     for i in range(data[fname_partial]):
969                         reader.next()
970
971     if not (mode == 'init' or 'id' in fields):
972         _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
973         return
974
975     uid = 1
976     datas = []
977     for line in reader:
978         if not (line and any(line)):
979             continue
980         try:
981             datas.append(map(misc.ustr, line))
982         except:
983             _logger.error("Cannot import the line: %s", line)
984
985     registry = openerp.registry(cr.dbname)
986     result, rows, warning_msg, dummy = registry[model].import_data(cr, uid, fields, datas,mode, module, noupdate, filename=fname_partial)
987     if result < 0:
988         # Report failed import and abort module install
989         raise Exception(_('Module loading failed: file %s/%s could not be processed:\n %s') % (module, fname, warning_msg))
990     if config.get('import_partial'):
991         data = pickle.load(file(config.get('import_partial')))
992         data[fname_partial] = 0
993         pickle.dump(data, file(config.get('import_partial'),'wb'))
994         cr.commit()
995
996 #
997 # xml import/export
998 #
999 def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
1000     doc = etree.parse(xmlfile)
1001     relaxng = etree.RelaxNG(
1002         etree.parse(os.path.join(config['root_path'],'import_xml.rng' )))
1003     try:
1004         relaxng.assert_(doc)
1005     except Exception:
1006         _logger.error('The XML file does not fit the required schema !')
1007         _logger.error(misc.ustr(relaxng.error_log.last_error))
1008         raise
1009
1010     if idref is None:
1011         idref={}
1012     obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
1013     obj.parse(doc.getroot())
1014     return True
1015
1016
1017 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: