1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4__all__ = [
5    'convert_file', 'convert_sql_import',
6    'convert_csv_import', 'convert_xml_import'
7]
8
9import base64
10import io
11import logging
12import os.path
13import re
14import subprocess
15import warnings
16
17from datetime import datetime, timedelta
18from dateutil.relativedelta import relativedelta
19
20import pytz
21from lxml import etree, builder
22try:
23    import jingtrang
24except ImportError:
25    jingtrang = None
26
27import odoo
28from . import pycompat
29from .config import config
30from .misc import file_open, unquote, ustr, SKIPPED_ELEMENT_TYPES
31from .translate import _
32from odoo import SUPERUSER_ID, api
33
34_logger = logging.getLogger(__name__)
35
36from .safe_eval import safe_eval as s_eval, pytz, time
37safe_eval = lambda expr, ctx={}: s_eval(expr, ctx, nocopy=True)
38
39class ParseError(Exception):
40    ...
41
42class RecordDictWrapper(dict):
43    """
44    Used to pass a record as locals in eval:
45    records do not strictly behave like dict, so we force them to.
46    """
47    def __init__(self, record):
48        self.record = record
49    def __getitem__(self, key):
50        if key in self.record:
51            return self.record[key]
52        return dict.__getitem__(self, key)
53
54def _get_idref(self, env, model_str, idref):
55    idref2 = dict(idref,
56                  time=time,
57                  DateTime=datetime,
58                  datetime=datetime,
59                  timedelta=timedelta,
60                  relativedelta=relativedelta,
61                  version=odoo.release.major_version,
62                  ref=self.id_get,
63                  pytz=pytz)
64    if model_str:
65        idref2['obj'] = env[model_str].browse
66    return idref2
67
68def _fix_multiple_roots(node):
69    """
70    Surround the children of the ``node`` element of an XML field with a
71    single root "data" element, to prevent having a document with multiple
72    roots once parsed separately.
73
74    XML nodes should have one root only, but we'd like to support
75    direct multiple roots in our partial documents (like inherited view architectures).
76    As a convention we'll surround multiple root with a container "data" element, to be
77    ignored later when parsing.
78    """
79    real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
80    if len(real_nodes) > 1:
81        data_node = etree.Element("data")
82        for child in node:
83            data_node.append(child)
84        node.append(data_node)
85
86def _eval_xml(self, node, env):
87    if node.tag in ('field','value'):
88        t = node.get('type','char')
89        f_model = node.get('model')
90        if node.get('search'):
91            f_search = node.get("search")
92            f_use = node.get("use",'id')
93            f_name = node.get("name")
94            idref2 = {}
95            if f_search:
96                idref2 = _get_idref(self, env, f_model, self.idref)
97            q = safe_eval(f_search, idref2)
98            ids = env[f_model].search(q).ids
99            if f_use != 'id':
100                ids = [x[f_use] for x in env[f_model].browse(ids).read([f_use])]
101            _fields = env[f_model]._fields
102            if (f_name in _fields) and _fields[f_name].type == 'many2many':
103                return ids
104            f_val = False
105            if len(ids):
106                f_val = ids[0]
107                if isinstance(f_val, tuple):
108                    f_val = f_val[0]
109            return f_val
110        a_eval = node.get('eval')
111        if a_eval:
112            idref2 = _get_idref(self, env, f_model, self.idref)
113            try:
114                return safe_eval(a_eval, idref2)
115            except Exception:
116                logging.getLogger('odoo.tools.convert.init').error(
117                    'Could not eval(%s) for %s in %s', a_eval, node.get('name'), env.context)
118                raise
119        def _process(s):
120            matches = re.finditer(br'[^%]%\((.*?)\)[ds]'.decode('utf-8'), s)
121            done = set()
122            for m in matches:
123                found = m.group()[1:]
124                if found in done:
125                    continue
126                done.add(found)
127                id = m.groups()[0]
128                if not id in self.idref:
129                    self.idref[id] = self.id_get(id)
130                # So funny story: in Python 3, bytes(n: int) returns a
131                # bytestring of n nuls. In Python 2 it obviously returns the
132                # stringified number, which is what we're expecting here
133                s = s.replace(found, str(self.idref[id]))
134            s = s.replace('%%', '%') # Quite weird but it's for (somewhat) backward compatibility sake
135            return s
136
137        if t == 'xml':
138            _fix_multiple_roots(node)
139            return '<?xml version="1.0"?>\n'\
140                +_process("".join(etree.tostring(n, encoding='unicode') for n in node))
141        if t == 'html':
142            return _process("".join(etree.tostring(n, encoding='unicode') for n in node))
143
144        data = node.text
145        if node.get('file'):
146            with file_open(node.get('file'), 'rb') as f:
147                data = f.read()
148
149        if t == 'base64':
150            return base64.b64encode(data)
151
152        # after that, only text content makes sense
153        data = pycompat.to_text(data)
154        if t == 'file':
155            from ..modules import module
156            path = data.strip()
157            if not module.get_module_resource(self.module, path):
158                raise IOError("No such file or directory: '%s' in %s" % (
159                    path, self.module))
160            return '%s,%s' % (self.module, path)
161
162        if t == 'char':
163            return data
164
165        if t == 'int':
166            d = data.strip()
167            if d == 'None':
168                return None
169            return int(d)
170
171        if t == 'float':
172            return float(data.strip())
173
174        if t in ('list','tuple'):
175            res=[]
176            for n in node.iterchildren(tag='value'):
177                res.append(_eval_xml(self, n, env))
178            if t=='tuple':
179                return tuple(res)
180            return res
181    elif node.tag == "function":
182        model_str = node.get('model')
183        model = env[model_str]
184        method_name = node.get('name')
185        # determine arguments
186        args = []
187        kwargs = {}
188        a_eval = node.get('eval')
189
190        if a_eval:
191            idref2 = _get_idref(self, env, model_str, self.idref)
192            args = list(safe_eval(a_eval, idref2))
193        for child in node:
194            if child.tag == 'value' and child.get('name'):
195                kwargs[child.get('name')] = _eval_xml(self, child, env)
196            else:
197                args.append(_eval_xml(self, child, env))
198        # merge current context with context in kwargs
199        kwargs['context'] = {**env.context, **kwargs.get('context', {})}
200        # invoke method
201        return odoo.api.call_kw(model, method_name, args, kwargs)
202    elif node.tag == "test":
203        return node.text
204
205
206def str2bool(value):
207    return value.lower() not in ('0', 'false', 'off')
208
209def nodeattr2bool(node, attr, default=False):
210    if not node.get(attr):
211        return default
212    val = node.get(attr).strip()
213    if not val:
214        return default
215    return str2bool(val)
216
217class xml_import(object):
218    def get_env(self, node, eval_context=None):
219        uid = node.get('uid')
220        context = node.get('context')
221        if uid or context:
222            return self.env(
223                user=uid and self.id_get(uid),
224                context=context and {
225                    **self.env.context,
226                    **safe_eval(context, {
227                        'ref': self.id_get,
228                        **(eval_context or {})
229                    })
230                }
231            )
232        return self.env
233
234    def make_xml_id(self, xml_id):
235        if not xml_id or '.' in xml_id:
236            return xml_id
237        return "%s.%s" % (self.module, xml_id)
238
239    def _test_xml_id(self, xml_id):
240        if '.' in xml_id:
241            module, id = xml_id.split('.', 1)
242            assert '.' not in id, """The ID reference "%s" must contain
243maximum one dot. They are used to refer to other modules ID, in the
244form: module.record_id""" % (xml_id,)
245            if module != self.module:
246                modcnt = self.env['ir.module.module'].search_count([('name', '=', module), ('state', '=', 'installed')])
247                assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
248
249    def _tag_delete(self, rec):
250        d_model = rec.get("model")
251        records = self.env[d_model]
252
253        d_search = rec.get("search")
254        if d_search:
255            idref = _get_idref(self, self.env, d_model, {})
256            try:
257                records = records.search(safe_eval(d_search, idref))
258            except ValueError:
259                _logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
260
261        d_id = rec.get("id")
262        if d_id:
263            try:
264                records += records.browse(self.id_get(d_id))
265            except ValueError:
266                # d_id cannot be found. doesn't matter in this case
267                _logger.warning('Skipping deletion for missing XML ID `%r`', d_id, exc_info=True)
268
269        if records:
270            records.unlink()
271
272    def _tag_report(self, rec):
273        res = {}
274        for dest,f in (('name','string'),('model','model'),('report_name','name')):
275            res[dest] = rec.get(f)
276            assert res[dest], "Attribute %s of report is empty !" % (f,)
277        for field, dest in (('attachment', 'attachment'),
278                            ('attachment_use', 'attachment_use'),
279                            ('usage', 'usage'),
280                            ('file', 'report_file'),
281                            ('report_type', 'report_type'),
282                            ('parser', 'parser'),
283                            ('print_report_name', 'print_report_name'),
284                            ):
285            if rec.get(field):
286                res[dest] = rec.get(field)
287        if rec.get('auto'):
288            res['auto'] = safe_eval(rec.get('auto','False'))
289        if rec.get('header'):
290            res['header'] = safe_eval(rec.get('header','False'))
291
292        res['multi'] = rec.get('multi') and safe_eval(rec.get('multi','False'))
293
294        xml_id = rec.get('id','')
295        self._test_xml_id(xml_id)
296        warnings.warn(f"The <report> tag is deprecated, use a <record> tag for {xml_id!r}.", DeprecationWarning)
297
298        if rec.get('groups'):
299            g_names = rec.get('groups','').split(',')
300            groups_value = []
301            for group in g_names:
302                if group.startswith('-'):
303                    group_id = self.id_get(group[1:])
304                    groups_value.append((3, group_id))
305                else:
306                    group_id = self.id_get(group)
307                    groups_value.append((4, group_id))
308            res['groups_id'] = groups_value
309        if rec.get('paperformat'):
310            pf_name = rec.get('paperformat')
311            pf_id = self.id_get(pf_name)
312            res['paperformat_id'] = pf_id
313
314        xid = self.make_xml_id(xml_id)
315        data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
316        report = self.env['ir.actions.report']._load_records([data], self.mode == 'update')
317        self.idref[xml_id] = report.id
318
319        if not rec.get('menu') or safe_eval(rec.get('menu','False')):
320            report.create_action()
321        elif self.mode=='update' and safe_eval(rec.get('menu','False'))==False:
322            # Special check for report having attribute menu=False on update
323            report.unlink_action()
324        return report.id
325
326    def _tag_function(self, rec):
327        if self.noupdate and self.mode != 'init':
328            return
329        env = self.get_env(rec)
330        _eval_xml(self, rec, env)
331
332    def _tag_act_window(self, rec):
333        name = rec.get('name')
334        xml_id = rec.get('id','')
335        self._test_xml_id(xml_id)
336        warnings.warn(f"The <act_window> tag is deprecated, use a <record> for {xml_id!r}.", DeprecationWarning)
337        view_id = False
338        if rec.get('view_id'):
339            view_id = self.id_get(rec.get('view_id'))
340        domain = rec.get('domain') or '[]'
341        res_model = rec.get('res_model')
342        binding_model = rec.get('binding_model')
343        view_mode = rec.get('view_mode') or 'tree,form'
344        usage = rec.get('usage')
345        limit = rec.get('limit')
346        uid = self.env.user.id
347
348        # Act_window's 'domain' and 'context' contain mostly literals
349        # but they can also refer to the variables provided below
350        # in eval_context, so we need to eval() them before storing.
351        # Among the context variables, 'active_id' refers to
352        # the currently selected items in a list view, and only
353        # takes meaning at runtime on the client side. For this
354        # reason it must remain a bare variable in domain and context,
355        # even after eval() at server-side. We use the special 'unquote'
356        # class to achieve this effect: a string which has itself, unquoted,
357        # as representation.
358        active_id = unquote("active_id")
359        active_ids = unquote("active_ids")
360        active_model = unquote("active_model")
361
362        # Include all locals() in eval_context, for backwards compatibility
363        eval_context = {
364            'name': name,
365            'xml_id': xml_id,
366            'type': 'ir.actions.act_window',
367            'view_id': view_id,
368            'domain': domain,
369            'res_model': res_model,
370            'src_model': binding_model,
371            'view_mode': view_mode,
372            'usage': usage,
373            'limit': limit,
374            'uid': uid,
375            'active_id': active_id,
376            'active_ids': active_ids,
377            'active_model': active_model,
378        }
379        context = self.get_env(rec, eval_context).context
380
381        try:
382            domain = safe_eval(domain, eval_context)
383        except (ValueError, NameError):
384            # Some domains contain references that are only valid at runtime at
385            # client-side, so in that case we keep the original domain string
386            # as it is. We also log it, just in case.
387            _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
388                'at server-side, keeping original string, in case it\'s meant for client side only',
389                domain, xml_id or 'n/a', exc_info=True)
390        res = {
391            'name': name,
392            'type': 'ir.actions.act_window',
393            'view_id': view_id,
394            'domain': domain,
395            'context': context,
396            'res_model': res_model,
397            'view_mode': view_mode,
398            'usage': usage,
399            'limit': limit,
400        }
401
402        if rec.get('groups'):
403            g_names = rec.get('groups','').split(',')
404            groups_value = []
405            for group in g_names:
406                if group.startswith('-'):
407                    group_id = self.id_get(group[1:])
408                    groups_value.append((3, group_id))
409                else:
410                    group_id = self.id_get(group)
411                    groups_value.append((4, group_id))
412            res['groups_id'] = groups_value
413
414        if rec.get('target'):
415            res['target'] = rec.get('target','')
416        if binding_model:
417            res['binding_model_id'] = self.env['ir.model']._get(binding_model).id
418            res['binding_type'] = rec.get('binding_type') or 'action'
419            views = rec.get('binding_views')
420            if views is not None:
421                res['binding_view_types'] = views
422        xid = self.make_xml_id(xml_id)
423        data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
424        self.env['ir.actions.act_window']._load_records([data], self.mode == 'update')
425
426    def _tag_menuitem(self, rec, parent=None):
427        rec_id = rec.attrib["id"]
428        self._test_xml_id(rec_id)
429
430        # The parent attribute was specified, if non-empty determine its ID, otherwise
431        # explicitly make a top-level menu
432        values = {
433            'parent_id': False,
434            'active': nodeattr2bool(rec, 'active', default=True),
435        }
436
437        if rec.get('sequence'):
438            values['sequence'] = int(rec.get('sequence'))
439
440        if parent is not None:
441            values['parent_id'] = parent
442        elif rec.get('parent'):
443            values['parent_id'] = self.id_get(rec.attrib['parent'])
444        elif rec.get('web_icon'):
445            values['web_icon'] = rec.attrib['web_icon']
446
447
448        if rec.get('name'):
449            values['name'] = rec.attrib['name']
450
451        if rec.get('action'):
452            a_action = rec.attrib['action']
453
454            if '.' not in a_action:
455                a_action = '%s.%s' % (self.module, a_action)
456            act = self.env.ref(a_action).sudo()
457            values['action'] = "%s,%d" % (act.type, act.id)
458
459            if not values.get('name') and act.type.endswith(('act_window', 'wizard', 'url', 'client', 'server')) and act.name:
460                values['name'] = act.name
461
462        if not values.get('name'):
463            values['name'] = rec_id or '?'
464
465
466        groups = []
467        for group in rec.get('groups', '').split(','):
468            if group.startswith('-'):
469                group_id = self.id_get(group[1:])
470                groups.append((3, group_id))
471            elif group:
472                group_id = self.id_get(group)
473                groups.append((4, group_id))
474        if groups:
475            values['groups_id'] = groups
476
477
478        data = {
479            'xml_id': self.make_xml_id(rec_id),
480            'values': values,
481            'noupdate': self.noupdate,
482        }
483        menu = self.env['ir.ui.menu']._load_records([data], self.mode == 'update')
484        for child in rec.iterchildren('menuitem'):
485            self._tag_menuitem(child, parent=menu.id)
486
487    def _tag_record(self, rec):
488        rec_model = rec.get("model")
489        env = self.get_env(rec)
490        rec_id = rec.get("id", '')
491
492        model = env[rec_model]
493
494        if self.xml_filename and rec_id:
495            model = model.with_context(
496                install_module=self.module,
497                install_filename=self.xml_filename,
498                install_xmlid=rec_id,
499            )
500
501        self._test_xml_id(rec_id)
502        xid = self.make_xml_id(rec_id)
503
504        # in update mode, the record won't be updated if the data node explicitly
505        # opt-out using @noupdate="1". A second check will be performed in
506        # model._load_records() using the record's ir.model.data `noupdate` field.
507        if self.noupdate and self.mode != 'init':
508            # check if the xml record has no id, skip
509            if not rec_id:
510                return None
511
512            record = env['ir.model.data']._load_xmlid(xid)
513            if record:
514                # if the resource already exists, don't update it but store
515                # its database id (can be useful)
516                self.idref[rec_id] = record.id
517                return None
518            elif not nodeattr2bool(rec, 'forcecreate', True):
519                # if it doesn't exist and we shouldn't create it, skip it
520                return None
521            # else create it normally
522
523        if xid and xid.partition('.')[0] != self.module:
524            # updating a record created by another module
525            record = self.env['ir.model.data']._load_xmlid(xid)
526            if not record:
527                if self.noupdate and not nodeattr2bool(rec, 'forcecreate', True):
528                    # if it doesn't exist and we shouldn't create it, skip it
529                    return None
530                raise Exception("Cannot update missing record %r" % xid)
531
532        res = {}
533        for field in rec.findall('./field'):
534            #TODO: most of this code is duplicated above (in _eval_xml)...
535            f_name = field.get("name")
536            f_ref = field.get("ref")
537            f_search = field.get("search")
538            f_model = field.get("model")
539            if not f_model and f_name in model._fields:
540                f_model = model._fields[f_name].comodel_name
541            f_use = field.get("use",'') or 'id'
542            f_val = False
543
544            if f_search:
545                idref2 = _get_idref(self, env, f_model, self.idref)
546                q = safe_eval(f_search, idref2)
547                assert f_model, 'Define an attribute model="..." in your .XML file !'
548                # browse the objects searched
549                s = env[f_model].search(q)
550                # column definitions of the "local" object
551                _fields = env[rec_model]._fields
552                # if the current field is many2many
553                if (f_name in _fields) and _fields[f_name].type == 'many2many':
554                    f_val = [(6, 0, [x[f_use] for x in s])]
555                elif len(s):
556                    # otherwise (we are probably in a many2one field),
557                    # take the first element of the search
558                    f_val = s[0][f_use]
559            elif f_ref:
560                if f_name in model._fields and model._fields[f_name].type == 'reference':
561                    val = self.model_id_get(f_ref)
562                    f_val = val[0] + ',' + str(val[1])
563                else:
564                    f_val = self.id_get(f_ref)
565            else:
566                f_val = _eval_xml(self, field, env)
567                if f_name in model._fields:
568                    field_type = model._fields[f_name].type
569                    if field_type == 'many2one':
570                        f_val = int(f_val) if f_val else False
571                    elif field_type == 'integer':
572                        f_val = int(f_val)
573                    elif field_type in ('float', 'monetary'):
574                        f_val = float(f_val)
575                    elif field_type == 'boolean' and isinstance(f_val, str):
576                        f_val = str2bool(f_val)
577            res[f_name] = f_val
578
579        data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
580        record = model._load_records([data], self.mode == 'update')
581        if rec_id:
582            self.idref[rec_id] = record.id
583        if config.get('import_partial'):
584            env.cr.commit()
585        return rec_model, record.id
586
587    def _tag_template(self, el):
588        # This helper transforms a <template> element into a <record> and forwards it
589        tpl_id = el.get('id', el.get('t-name'))
590        full_tpl_id = tpl_id
591        if '.' not in full_tpl_id:
592            full_tpl_id = '%s.%s' % (self.module, tpl_id)
593        # set the full template name for qweb <module>.<id>
594        if not el.get('inherit_id'):
595            el.set('t-name', full_tpl_id)
596            el.tag = 't'
597        else:
598            el.tag = 'data'
599        el.attrib.pop('id', None)
600
601        if self.module.startswith('theme_'):
602            model = 'theme.ir.ui.view'
603        else:
604            model = 'ir.ui.view'
605
606        record_attrs = {
607            'id': tpl_id,
608            'model': model,
609        }
610        for att in ['forcecreate', 'context']:
611            if att in el.attrib:
612                record_attrs[att] = el.attrib.pop(att)
613
614        Field = builder.E.field
615        name = el.get('name', tpl_id)
616
617        record = etree.Element('record', attrib=record_attrs)
618        record.append(Field(name, name='name'))
619        record.append(Field(full_tpl_id, name='key'))
620        record.append(Field("qweb", name='type'))
621        if 'track' in el.attrib:
622            record.append(Field(el.get('track'), name='track'))
623        if 'priority' in el.attrib:
624            record.append(Field(el.get('priority'), name='priority'))
625        if 'inherit_id' in el.attrib:
626            record.append(Field(name='inherit_id', ref=el.get('inherit_id')))
627        if 'website_id' in el.attrib:
628            record.append(Field(name='website_id', ref=el.get('website_id')))
629        if 'key' in el.attrib:
630            record.append(Field(el.get('key'), name='key'))
631        if el.get('active') in ("True", "False"):
632            view_id = self.id_get(tpl_id, raise_if_not_found=False)
633            if self.mode != "update" or not view_id:
634                record.append(Field(name='active', eval=el.get('active')))
635        if el.get('customize_show') in ("True", "False"):
636            record.append(Field(name='customize_show', eval=el.get('customize_show')))
637        groups = el.attrib.pop('groups', None)
638        if groups:
639            grp_lst = [("ref('%s')" % x) for x in groups.split(',')]
640            record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
641        if el.get('primary') == 'True':
642            # Pseudo clone mode, we'll set the t-name to the full canonical xmlid
643            el.append(
644                builder.E.xpath(
645                    builder.E.attribute(full_tpl_id, name='t-name'),
646                    expr=".",
647                    position="attributes",
648                )
649            )
650            record.append(Field('primary', name='mode'))
651        # inject complete <template> element (after changing node name) into
652        # the ``arch`` field
653        record.append(Field(el, name="arch", type="xml"))
654
655        return self._tag_record(record)
656
657    def id_get(self, id_str, raise_if_not_found=True):
658        if id_str in self.idref:
659            return self.idref[id_str]
660        res = self.model_id_get(id_str, raise_if_not_found)
661        return res and res[1]
662
663    def model_id_get(self, id_str, raise_if_not_found=True):
664        if '.' not in id_str:
665            id_str = '%s.%s' % (self.module, id_str)
666        return self.env['ir.model.data'].xmlid_to_res_model_res_id(id_str, raise_if_not_found=raise_if_not_found)
667
668    def _tag_root(self, el):
669        for rec in el:
670            f = self._tags.get(rec.tag)
671            if f is None:
672                continue
673
674            self.envs.append(self.get_env(el))
675            self._noupdate.append(nodeattr2bool(el, 'noupdate', self.noupdate))
676            try:
677                f(rec)
678            except ParseError:
679                raise
680            except Exception as e:
681                raise ParseError('while parsing %s:%s, near\n%s' % (
682                    rec.getroottree().docinfo.URL,
683                    rec.sourceline,
684                    etree.tostring(rec, encoding='unicode').rstrip()
685                )) from e
686            finally:
687                self._noupdate.pop()
688                self.envs.pop()
689
690    @property
691    def env(self):
692        return self.envs[-1]
693
694    @property
695    def noupdate(self):
696        return self._noupdate[-1]
697
698    def __init__(self, cr, module, idref, mode, noupdate=False, xml_filename=None):
699        self.mode = mode
700        self.module = module
701        self.envs = [odoo.api.Environment(cr, SUPERUSER_ID, {})]
702        self.idref = {} if idref is None else idref
703        self._noupdate = [noupdate]
704        self.xml_filename = xml_filename
705        self._tags = {
706            'record': self._tag_record,
707            'delete': self._tag_delete,
708            'function': self._tag_function,
709            'menuitem': self._tag_menuitem,
710            'template': self._tag_template,
711            'report': self._tag_report,
712            'act_window': self._tag_act_window,
713
714            **dict.fromkeys(self.DATA_ROOTS, self._tag_root)
715        }
716
717    def parse(self, de):
718        assert de.tag in self.DATA_ROOTS, "Root xml tag must be <openerp>, <odoo> or <data>."
719        self._tag_root(de)
720    DATA_ROOTS = ['odoo', 'data', 'openerp']
721
722def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, pathname=None):
723    if pathname is None:
724        pathname = os.path.join(module, filename)
725    ext = os.path.splitext(filename)[1].lower()
726
727    with file_open(pathname, 'rb') as fp:
728        if ext == '.csv':
729            convert_csv_import(cr, module, pathname, fp.read(), idref, mode, noupdate)
730        elif ext == '.sql':
731            convert_sql_import(cr, fp)
732        elif ext == '.xml':
733            convert_xml_import(cr, module, fp, idref, mode, noupdate)
734        elif ext == '.js':
735            pass # .js files are valid but ignored here.
736        else:
737            raise ValueError("Can't load unknown file type %s.", filename)
738
739def convert_sql_import(cr, fp):
740    cr.execute(fp.read())
741
742def convert_csv_import(cr, module, fname, csvcontent, idref=None, mode='init',
743        noupdate=False):
744    '''Import csv file :
745        quote: "
746        delimiter: ,
747        encoding: utf-8'''
748    filename, _ext = os.path.splitext(os.path.basename(fname))
749    model = filename.split('-')[0]
750    reader = pycompat.csv_reader(io.BytesIO(csvcontent), quotechar='"', delimiter=',')
751    fields = next(reader)
752
753    if not (mode == 'init' or 'id' in fields):
754        _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
755        return
756
757    # filter out empty lines (any([]) == False) and lines containing only empty cells
758    datas = [
759        line for line in reader
760        if any(line)
761    ]
762
763    context = {
764        'mode': mode,
765        'module': module,
766        'install_module': module,
767        'install_filename': fname,
768        'noupdate': noupdate,
769    }
770    env = odoo.api.Environment(cr, SUPERUSER_ID, context)
771    result = env[model].load(fields, datas)
772    if any(msg['type'] == 'error' for msg in result['messages']):
773        # Report failed import and abort module install
774        warning_msg = "\n".join(msg['message'] for msg in result['messages'])
775        raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
776
777def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
778    doc = etree.parse(xmlfile)
779    schema = os.path.join(config['root_path'], 'import_xml.rng')
780    relaxng = etree.RelaxNG(etree.parse(schema))
781    try:
782        relaxng.assert_(doc)
783    except Exception:
784        _logger.exception("The XML file '%s' does not fit the required schema !", xmlfile.name)
785        if jingtrang:
786            p = subprocess.run(['pyjing', schema, xmlfile.name], stdout=subprocess.PIPE)
787            _logger.warning(p.stdout.decode())
788        else:
789            for e in relaxng.error_log:
790                _logger.warning(e)
791            _logger.info("Install 'jingtrang' for more precise and useful validation messages.")
792        raise
793
794    if isinstance(xmlfile, str):
795        xml_filename = xmlfile
796    else:
797        xml_filename = xmlfile.name
798    obj = xml_import(cr, module, idref, mode, noupdate=noupdate, xml_filename=xml_filename)
799    obj.parse(doc.getroot())
800