mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 09:52:02 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
|
|
@ -0,0 +1,5 @@
|
|||
from . import utils
|
||||
from . import models
|
||||
from . import ir_exports
|
||||
from . import ir_exports_line
|
||||
from . import ir_exports_resolver
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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])
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue