18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -5,20 +5,17 @@ __all__ = [
'convert_file', 'convert_sql_import',
'convert_csv_import', 'convert_xml_import'
]
import base64
import csv
import io
import logging
import os.path
import pprint
import re
import subprocess
import warnings
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import pytz
from dateutil.relativedelta import relativedelta
from lxml import etree, builder
try:
import jingtrang
@ -26,33 +23,22 @@ except ImportError:
jingtrang = None
import odoo
from . import pycompat
from .config import config
from .misc import file_open, file_path, SKIPPED_ELEMENT_TYPES
from .translate import _
from odoo import SUPERUSER_ID, api
from odoo.exceptions import ValidationError
from .safe_eval import safe_eval as s_eval, pytz, time
_logger = logging.getLogger(__name__)
from .safe_eval import safe_eval as s_eval, pytz, time
safe_eval = lambda expr, ctx={}: s_eval(expr, ctx, nocopy=True)
def safe_eval(expr, ctx={}):
return s_eval(expr, ctx, nocopy=True)
class ParseError(Exception):
...
class RecordDictWrapper(dict):
"""
Used to pass a record as locals in eval:
records do not strictly behave like dict, so we force them to.
"""
def __init__(self, record):
self.record = record
def __getitem__(self, key):
if key in self.record:
return self.record[key]
return dict.__getitem__(self, key)
def _get_idref(self, env, model_str, idref):
idref2 = dict(idref,
Command=odoo.fields.Command,
@ -90,8 +76,7 @@ def _eval_xml(self, node, env):
if node.tag in ('field','value'):
t = node.get('type','char')
f_model = node.get('model')
if node.get('search'):
f_search = node.get("search")
if f_search := node.get('search'):
f_use = node.get("use",'id')
f_name = node.get("name")
idref2 = {}
@ -110,8 +95,7 @@ def _eval_xml(self, node, env):
if isinstance(f_val, tuple):
f_val = f_val[0]
return f_val
a_eval = node.get('eval')
if a_eval:
if a_eval := node.get('eval'):
idref2 = _get_idref(self, env, f_model, self.idref)
try:
return safe_eval(a_eval, idref2)
@ -144,44 +128,44 @@ def _eval_xml(self, node, env):
if t == 'html':
return _process("".join(etree.tostring(n, method='html', encoding='unicode') for n in node))
data = node.text
if node.get('file'):
with file_open(node.get('file'), 'rb', env=env) as f:
if t == 'base64':
with file_open(node.get('file'), 'rb', env=env) as f:
return base64.b64encode(f.read())
with file_open(node.get('file'), env=env) as f:
data = f.read()
else:
data = node.text or ''
if t == 'base64':
return base64.b64encode(data)
match t:
case 'file':
path = data.strip()
try:
file_path(os.path.join(self.module, path))
except FileNotFoundError:
raise FileNotFoundError(
f"No such file or directory: {path!r} in {self.module}"
) from None
return '%s,%s' % (self.module, path)
case 'char':
return data
case 'int':
d = data.strip()
if d == 'None':
return None
return int(d)
case 'float':
return float(data.strip())
case 'list':
return [_eval_xml(self, n, env) for n in node.iterchildren('value')]
case 'tuple':
return tuple(_eval_xml(self, n, env) for n in node.iterchildren('value'))
case 'base64':
raise ValueError("base64 type is only compatible with file data")
case t:
raise ValueError(f"Unknown type {t!r}")
# after that, only text content makes sense
data = pycompat.to_text(data)
if t == 'file':
path = data.strip()
try:
file_path(os.path.join(self.module, path))
except FileNotFoundError:
raise IOError("No such file or directory: '%s' in %s" % (
path, self.module))
return '%s,%s' % (self.module, path)
if t == 'char':
return data
if t == 'int':
d = data.strip()
if d == 'None':
return None
return int(d)
if t == 'float':
return float(data.strip())
if t in ('list','tuple'):
res=[]
for n in node.iterchildren(tag='value'):
res.append(_eval_xml(self, n, env))
if t=='tuple':
return tuple(res)
return res
elif node.tag == "function":
model_str = node.get('model')
model = env[model_str]
@ -189,9 +173,8 @@ def _eval_xml(self, node, env):
# determine arguments
args = []
kwargs = {}
a_eval = node.get('eval')
if a_eval:
if a_eval := node.get('eval'):
idref2 = _get_idref(self, env, model_str, self.idref)
args = list(safe_eval(a_eval, idref2))
for child in node:
@ -254,16 +237,14 @@ form: module.record_id""" % (xml_id,)
d_model = rec.get("model")
records = self.env[d_model]
d_search = rec.get("search")
if d_search:
if d_search := rec.get("search"):
idref = _get_idref(self, self.env, d_model, {})
try:
records = records.search(safe_eval(d_search, idref))
except ValueError:
_logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
d_id = rec.get("id")
if d_id:
if d_id := rec.get("id"):
try:
records += records.browse(self.id_get(d_id))
except ValueError:
@ -365,16 +346,14 @@ form: module.record_id""" % (xml_id,)
if not rec_id:
return None
record = env['ir.model.data']._load_xmlid(xid)
for child in rec.xpath('.//record[@id]'):
sub_xid = child.get("id")
self._test_xml_id(sub_xid)
sub_xid = self.make_xml_id(sub_xid)
sub_record = env['ir.model.data']._load_xmlid(sub_xid)
if sub_record:
self.idref[sub_xid] = sub_record.id
if record := env['ir.model.data']._load_xmlid(xid):
for child in rec.xpath('.//record[@id]'):
sub_xid = child.get("id")
self._test_xml_id(sub_xid)
sub_xid = self.make_xml_id(sub_xid)
if sub_record := env['ir.model.data']._load_xmlid(sub_xid):
self.idref[sub_xid] = sub_record.id
if record:
# if the resource already exists, don't update it but store
# its database id (can be useful)
self.idref[rec_id] = record.id
@ -384,10 +363,11 @@ form: module.record_id""" % (xml_id,)
return None
# else create it normally
foreign_record_to_create = False
if xid and xid.partition('.')[0] != self.module:
# updating a record created by another module
record = self.env['ir.model.data']._load_xmlid(xid)
if not record:
if not record and not (foreign_record_to_create := nodeattr2bool(rec, 'forcecreate')): # Allow foreign records if explicitely stated
if self.noupdate and not nodeattr2bool(rec, 'forcecreate', True):
# if it doesn't exist and we shouldn't create it, skip it
return None
@ -395,18 +375,16 @@ form: module.record_id""" % (xml_id,)
res = {}
sub_records = []
for field in rec.findall('./field'):
for field in rec.iterchildren('field'):
#TODO: most of this code is duplicated above (in _eval_xml)...
f_name = field.get("name")
f_ref = field.get("ref")
f_search = field.get("search")
f_model = field.get("model")
if not f_model and f_name in model._fields:
f_model = model._fields[f_name].comodel_name
f_use = field.get("use",'') or 'id'
f_val = False
if f_search:
if f_search := field.get("search"):
idref2 = _get_idref(self, env, f_model, self.idref)
q = safe_eval(f_search, idref2)
assert f_model, 'Define an attribute model="..." in your .XML file!'
@ -421,7 +399,7 @@ form: module.record_id""" % (xml_id,)
# otherwise (we are probably in a many2one field),
# take the first element of the search
f_val = s[0][f_use]
elif f_ref:
elif f_ref := field.get("ref"):
if f_name in model._fields and model._fields[f_name].type == 'reference':
val = self.model_id_get(f_ref)
f_val = val[0] + ',' + str(val[1])
@ -443,7 +421,7 @@ form: module.record_id""" % (xml_id,)
elif field_type == 'boolean' and isinstance(f_val, str):
f_val = str2bool(f_val)
elif field_type == 'one2many':
for child in field.findall('./record'):
for child in field.iterchildren('record'):
sub_records.append((child, model._fields[f_name].inverse_name))
if isinstance(f_val, str):
# We do not want to write on the field since we will write
@ -461,6 +439,8 @@ form: module.record_id""" % (xml_id,)
res['sequence'] = sequence
data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
if foreign_record_to_create:
model = model.with_context(foreign_record_to_create=foreign_record_to_create)
record = model._load_records([data], self.mode == 'update')
if rec_id:
self.idref[rec_id] = record.id
@ -650,7 +630,7 @@ def convert_csv_import(env, module, fname, csvcontent, idref=None, mode='init',
env = env(context=dict(env.context, lang=None))
filename, _ext = os.path.splitext(os.path.basename(fname))
model = filename.split('-')[0]
reader = pycompat.csv_reader(io.BytesIO(csvcontent), quotechar='"', delimiter=',')
reader = csv.reader(io.StringIO(csvcontent.decode()), quotechar='"', delimiter=',')
fields = next(reader)
if not (mode == 'init' or 'id' in fields):
@ -674,7 +654,12 @@ def convert_csv_import(env, module, fname, csvcontent, idref=None, mode='init',
if any(msg['type'] == 'error' for msg in result['messages']):
# Report failed import and abort module install
warning_msg = "\n".join(msg['message'] for msg in result['messages'])
raise Exception(_('Module loading %s failed: file %s could not be processed:\n %s') % (module, fname, warning_msg))
raise Exception(env._(
"Module loading %(module)s failed: file %(file)s could not be processed:\n%(message)s",
module=module,
file=fname,
message=warning_msg,
))
def convert_xml_import(env, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
doc = etree.parse(xmlfile)