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