Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,5 @@
from . import utils
from . import models
from . import ir_exports
from . import ir_exports_line
from . import ir_exports_resolver

View file

@ -0,0 +1,124 @@
# © 2017 Akretion (http://www.akretion.com)
# Sébastien BEAU <sebastien.beau@akretion.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from collections import OrderedDict
from odoo import fields, models
from odoo.tools import ormcache
def partition(line, accessor):
"""Partition a recordset according to an accessor (e.g. a lambda).
Returns a dictionary whose keys are the values obtained from accessor,
and values are the items that have this value.
Example: partition([{"name": "ax"}, {"name": "by"}], lambda x: "x" in x["name"])
=> {True: [{"name": "ax"}], False: [{"name": "by"}]}
"""
result = {}
for item in line:
key = accessor(item)
if key not in result:
result[key] = []
result[key].append(item)
return result
def update_dict(data, fields, options):
"""Contruct a tree of fields.
Example:
{
"name": True,
"resource": True,
}
Order of keys is important.
"""
field = fields[0]
if len(fields) == 1:
if field == ".id":
field = "id"
data[field] = (True, options)
else:
if field not in data:
data[field] = (False, OrderedDict())
update_dict(data[field][1], fields[1:], options)
def convert_dict(dict_parser):
"""Convert dict returned by update_dict to list consistent w/ Odoo API.
The list is composed of strings (field names or targets) or tuples.
"""
parser = []
for field, value in dict_parser.items():
if value[0] is True: # is a leaf
parser.append(field_dict(field, value[1]))
else:
parser.append((field_dict(field), convert_dict(value[1])))
return parser
def field_dict(field, options=None):
"""Create a parser dict for the field field."""
result = {"name": field.split(":")[0]}
if len(field.split(":")) > 1:
result["target"] = field.split(":")[1]
for option in options or {}:
if options[option]:
result[option] = options[option]
return result
class IrExports(models.Model):
_inherit = "ir.exports"
language_agnostic = fields.Boolean(
default=False,
help="If set, will set the lang to False when exporting lines without lang,"
" otherwise it uses the lang in the given context to export these fields",
)
global_resolver_id = fields.Many2one(
comodel_name="ir.exports.resolver",
string="Custom global resolver",
domain="[('type', '=', 'global')]",
help="If set, will apply the global resolver to the result",
)
@ormcache(
"self.language_agnostic",
"self.global_resolver_id.id",
"tuple(self.export_fields.mapped('write_date'))",
)
def get_json_parser(self):
"""Creates a parser from ir.exports record and return it.
The final parser can be used to "jsonify" records of ir.export's model.
"""
self.ensure_one()
parser = {}
lang_to_lines = partition(self.export_fields, lambda l: l.lang_id.code)
lang_parsers = {}
for lang in lang_to_lines:
dict_parser = OrderedDict()
for line in lang_to_lines[lang]:
names = line.name.split("/")
if line.target:
names = line.target.split("/")
function = line.instance_method_name
# resolver must be passed as ID to avoid cache issues
options = {"resolver": line.resolver_id.id, "function": function}
update_dict(dict_parser, names, options)
lang_parsers[lang] = convert_dict(dict_parser)
if list(lang_parsers.keys()) == [False]:
parser["fields"] = lang_parsers[False]
else:
parser["langs"] = lang_parsers
if self.global_resolver_id:
parser["resolver"] = self.global_resolver_id.id
if self.language_agnostic:
parser["language_agnostic"] = self.language_agnostic
return parser

View file

@ -0,0 +1,58 @@
# Copyright 2017 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class IrExportsLine(models.Model):
_inherit = "ir.exports.line"
target = fields.Char(
help="The complete path to the field where you can specify a "
"target on the step as field:target",
)
active = fields.Boolean(default=True)
lang_id = fields.Many2one(
comodel_name="res.lang",
string="Language",
help="If set, the language in which the field is exported",
)
resolver_id = fields.Many2one(
comodel_name="ir.exports.resolver",
string="Custom resolver",
help="If set, will apply the resolver on the field value",
)
instance_method_name = fields.Char(
string="Function",
help="A method defined on the model that takes a record and a field_name",
)
@api.constrains("resolver_id", "instance_method_name")
def _check_function_resolver(self):
for rec in self:
if rec.resolver_id and rec.instance_method_name:
msg = _("Either set a function or a resolver, not both.")
raise ValidationError(msg)
@api.constrains("target", "name")
def _check_target(self):
for rec in self:
if not rec.target:
continue
names = rec.name.split("/")
names_with_target = rec.target.split("/")
if len(names) != len(names_with_target):
raise ValidationError(
_("Name and Target must have the same hierarchy depth")
)
for name, name_with_target in zip(names, names_with_target):
field_name = name_with_target.split(":")[0]
if name != field_name:
raise ValidationError(
_(
"The target must reference the same field as in "
"name '%(name)s' not in '%(name_with_target)s'"
)
% dict(name=name, name_with_target=name_with_target)
)

View file

@ -0,0 +1,58 @@
# Copyright 2020 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import fields, models
from odoo.tools.safe_eval import safe_eval
help_message = [
"Compute the result from 'value' by setting the variable 'result'.",
"\n" "For fields resolvers:",
":param record: the record",
":param name: name of the field",
":param value: value of the field",
":param field_type: type of the field",
"\n" "For global resolvers:",
":param value: JSON dict",
":param record: the record",
"\n"
"In both types, you can override the final json key."
"\nTo achieve this, simply return a dict like: "
"\n{'result': {'_value': $value, '_json_key': $new_json_key}}",
]
class FieldResolver(models.Model):
"""Arbitrary function to process a field or a dict at export time."""
_name = "ir.exports.resolver"
_description = "Export Resolver"
name = fields.Char()
type = fields.Selection([("field", "Field"), ("global", "Global")])
python_code = fields.Text(
default="\n".join(["# " + h for h in help_message] + ["result = value"]),
help="\n".join(help_message),
)
def resolve(self, param, records):
self.ensure_one()
result = []
context = records.env.context
if self.type == "global":
assert len(param) == len(records)
for value, record in zip(param, records):
values = {"value": value, "record": record, "context": context}
safe_eval(self.python_code, values, mode="exec", nocopy=True)
result.append(values["result"])
else: # param is a field
for record in records:
values = {
"record": record,
"value": record[param.name],
"name": param.name,
"field_type": param.type,
"context": context,
}
safe_eval(self.python_code, values, mode="exec", nocopy=True)
result.append(values["result"])
return result

View file

@ -0,0 +1,267 @@
# Copyright 2017 Akretion (http://www.akretion.com)
# Sébastien BEAU <sebastien.beau@akretion.com>
# Raphaël Reverdy <raphael.reverdy@akretion.com>
# Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
# Simone Orsi <simahawk@gmail.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import logging
from odoo import api, fields, models, tools
from odoo.exceptions import UserError
from odoo.tools.misc import format_duration
from odoo.tools.translate import _
from ..exceptions import SwallableException
from .utils import convert_simple_to_full_parser
_logger = logging.getLogger(__name__)
class Base(models.AbstractModel):
_inherit = "base"
@api.model
def __parse_field(self, parser_field):
"""Deduct how to handle a field from its parser."""
return parser_field if isinstance(parser_field, tuple) else (parser_field, None)
@api.model
def _jsonify_bad_parser_error(self, field_name):
raise UserError(_("Wrong parser configuration for field: `%s`") % field_name)
def _function_value(self, record, function, field_name):
if function in dir(record):
method = getattr(record, function, None)
return method(field_name)
elif callable(function):
return function(record, field_name)
else:
return self._jsonify_bad_parser_error(field_name)
@api.model
def _jsonify_value(self, field, value):
"""Override this function to support new field types."""
if value is False and field.type != "boolean":
value = None
elif field.type == "date":
value = fields.Date.to_date(value).isoformat()
elif field.type == "datetime":
# Ensures value is a datetime
value = fields.Datetime.to_datetime(value)
value = value.isoformat()
elif field.type in ("many2one", "reference"):
value = value.display_name if value else None
elif field.type in ("one2many", "many2many"):
value = [v.display_name for v in value]
return value
@api.model
def _add_json_key(self, values, json_key, value):
"""To manage defaults, you can use a specific resolver."""
key, sep, marshaller = json_key.partition("=")
if marshaller == "list": # sublist field
if not values.get(key):
values[key] = []
values[key].append(value)
else:
values[key] = value
@api.model
def _jsonify_record(self, parser, rec, root):
"""JSONify one record (rec). Private function called by jsonify."""
strict = self.env.context.get("jsonify_record_strict", False)
for field in parser:
field_dict, subparser = rec.__parse_field(field)
function = field_dict.get("function")
try:
self._jsonify_record_validate_field(rec, field_dict, strict)
except SwallableException:
if not function:
continue
json_key = field_dict.get("target", field_dict["name"])
if function:
try:
value = self._jsonify_record_handle_function(
rec, field_dict, strict
)
except SwallableException:
continue
elif subparser:
try:
value = self._jsonify_record_handle_subparser(
rec, field_dict, strict, subparser
)
except SwallableException:
continue
else:
field = rec._fields[field_dict["name"]]
value = rec._jsonify_value(field, rec[field.name])
resolver = field_dict.get("resolver")
if resolver:
if isinstance(resolver, int):
# cached versions of the parser are stored as integer
resolver = self.env["ir.exports.resolver"].browse(resolver)
value, json_key = self._jsonify_record_handle_resolver(
rec, field, resolver, json_key
)
# whatever json value we have found in subparser or not ass a sister key
# on the same level _fieldname_{json_key}
if rec.env.context.get("with_fieldname"):
json_key_fieldname = "_fieldname_" + json_key
# check if we are in a subparser has already the fieldname sister keys
fieldname_value = rec._fields[field_dict["name"]].string
self._add_json_key(root, json_key_fieldname, fieldname_value)
self._add_json_key(root, json_key, value)
return root
def _jsonify_record_validate_field(self, rec, field_dict, strict):
field_name = field_dict["name"]
if field_name not in rec._fields:
if strict:
# let it fail
rec._fields[field_name] # pylint: disable=pointless-statement
else:
if not tools.config["test_enable"]:
# If running live, log proper error
# so that techies can track it down
_logger.warning(
"%(model)s.%(fname)s not available",
{"model": self._name, "fname": field_name},
)
raise SwallableException()
return True
def _jsonify_record_handle_function(self, rec, field_dict, strict):
field_name = field_dict["name"]
function = field_dict["function"]
try:
return self._function_value(rec, function, field_name)
except UserError as err:
if strict:
raise
if not tools.config["test_enable"]:
_logger.error(
"%(model)s.%(func)s not available",
{"model": self._name, "func": str(function)},
)
raise SwallableException() from err
def _jsonify_record_handle_subparser(self, rec, field_dict, strict, subparser):
field_name = field_dict["name"]
field = rec._fields[field_name]
if not (field.relational or field.type == "reference"):
if strict:
self._jsonify_bad_parser_error(field_name)
if not tools.config["test_enable"]:
_logger.error(
"%(model)s.%(fname)s not relational",
{"model": self._name, "fname": field_name},
)
raise SwallableException()
value = [self._jsonify_record(subparser, r, {}) for r in rec[field_name]]
if field.type in ("many2one", "reference"):
value = value[0] if value else None
return value
def _jsonify_record_handle_resolver(self, rec, field, resolver, json_key):
value = rec._jsonify_value(field, rec[field.name])
value = resolver.resolve(field, rec)[0] if resolver else value
if isinstance(value, dict) and "_json_key" in value and "_value" in value:
# Allow override of json_key.
# In this case,
# the final value must be encapsulated into _value key
value, json_key = value["_value"], value["_json_key"]
return value, json_key
def jsonify(self, parser, one=False, with_fieldname=False):
"""Convert the record according to the given parser.
Example of (simple) parser:
parser = [
'name',
'number',
'create_date',
('partner_id', ['id', 'display_name', 'ref'])
('shipping_id', callable)
('delivery_id', "record_method")
('line_id', ['id', ('product_id', ['name']), 'price_unit'])
]
In order to be consistent with the Odoo API the jsonify method always
returns a list of objects even if there is only one element in input.
You can change this behavior by passing `one=True` to get only one element.
By default the key into the JSON is the name of the field extracted
from the model. If you need to specify an alternate name to use as
key, you can define your mapping as follow into the parser definition:
parser = [
'field_name:json_key'
]
"""
if one:
self.ensure_one()
if isinstance(parser, list):
parser = convert_simple_to_full_parser(parser)
resolver = parser.get("resolver")
if isinstance(resolver, int):
# cached versions of the parser are stored as integer
resolver = self.env["ir.exports.resolver"].browse(resolver)
results = [{} for record in self]
parsers = {False: parser["fields"]} if "fields" in parser else parser["langs"]
for lang in parsers:
translate = lang or parser.get("language_agnostic")
new_ctx = {}
if translate:
new_ctx["lang"] = lang
if with_fieldname:
new_ctx["with_fieldname"] = True
records = self.with_context(**new_ctx) if new_ctx else self
for record, json in zip(records, results):
self._jsonify_record(parsers[lang], record, json)
if resolver:
results = resolver.resolve(results, self)
return results[0] if one else results
# HELPERS
def _jsonify_m2o_to_id(self, fname):
"""Helper to get an ID only from a m2o field.
Example:
<field name="name">m2o_id</field>
<field name="target">m2o_id:rel_id</field>
<field name="instance_method_name">_jsonify_m2o_to_id</field>
"""
return self[fname].id
def _jsonify_x2m_to_ids(self, fname):
"""Helper to get a list of IDs only from a o2m or m2m field.
Example:
<field name="name">m2m_ids</field>
<field name="target">m2m_ids:rel_ids</field>
<field name="instance_method_name">_jsonify_x2m_to_ids</field>
"""
return self[fname].ids
def _jsonify_format_duration(self, fname):
"""Helper to format a Float-like duration to string 00:00.
Example:
<field name="name">duration</field>
<field name="instance_method_name">_jsonify_format_duration</field>
"""
return format_duration(self[fname])

View file

@ -0,0 +1,35 @@
def convert_simple_to_full_parser(parser):
"""Convert a simple API style parser to a full parser"""
assert isinstance(parser, list)
return {"fields": _convert_parser(parser)}
def _convert_field(fld, function=None):
"""Return a dict from the string encoding a field to export.
The : is used as a separator to specify a target, if any.
"""
name, sep, target = fld.partition(":")
field_dict = {"name": name}
if target:
field_dict["target"] = target
if function:
field_dict["function"] = function
return field_dict
def _convert_parser(parser):
"""Recursively process each list to replace encoded fields as string
by dicts specifying each attribute by its relevant key.
"""
result = []
for line in parser:
if isinstance(line, str):
field_def = _convert_field(line)
else:
fld, sub = line
if callable(sub) or isinstance(sub, str):
field_def = _convert_field(fld, sub)
else:
field_def = (_convert_field(fld), _convert_parser(sub))
result.append(field_def)
return result