[ADD] new openapi modules

This commit is contained in:
erp committed 2021-01-03 22:07:15 -08:00
1 parent a7916efebd
commit bd89cf9097
54 files changed
+5094

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: https://www.gnu.org/licenses/lgpl
:alt: License: LGPL-3
==========
Base Api
==========
Module extends *base*-model in purpose of creating and quering objects.
Credits
=======
Contributors
------------
* `Anvar Kildebekov <https://it-projects.info/team/fedoranvar>`__:
- :one::three: init version of the module
Sponsors
--------
* `IT-Projects LLC <https://it-projects.info>`__
Maintainers
-----------
* `IT-Projects LLC <https://it-projects.info>`__
To get a guaranteed support
you are kindly requested to purchase the module
at `odoo apps store <https://apps.odoo.com/apps/modules/13.0/base_api/>`__.
Thank you for understanding!
`IT-Projects Team <https://www.it-projects.info/team>`__
Further information
===================
Demo: http://runbot.it-projects.info/demo/sync-addons/13.0
HTML Description: https://apps.odoo.com/apps/modules/13.0/base_api/
Usage instructions: `<doc/index.rst>`__
Changelog: `<doc/changelog.rst>`__
Notifications on updates: `via Atom
<https://github.com/it-projects-llc/sync-addons/commits/13.0/base_api.atom>`__,
`by Email
<https://blogtrottr.com/?subscribe=https://github.com/it-projects-llc/sync-addons/commits/13.0/base_api.atom>`__
Tested on Odoo 13.0 eed3171f2f057223c3fbedd95ce6bcde2124f46c
+4
View File
@@ -0,0 +1,4 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from . import models
from . import lib
+34
View File
@@ -0,0 +1,34 @@
# Copyright 2019 Anvar Kildebekov <https://it-projects.info/team/fedoranvar>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
"name": """Base API""",
"summary": """Basic function and methods of API for openapi or XML-RPC""",
"category": "Hidden",
# "live_test_url": "http://apps.it-projects.info/shop/product/DEMO-URL?version=13.0",
"version": "13.0.1.0.1",
"application": False,
"author": "FlectraHQ, IT-Projects LLC, Anvar Kildebekov",
"license": "LGPL-3",
"depends": [],
"external_dependencies": {"python": [], "bin": []},
"data": [],
"demo": [],
"qweb": [],
"post_load": None,
"pre_init_hook": None,
"post_init_hook": None,
"uninstall_hook": None,
"auto_install": False,
"installable": True,
# "demo_title": "Base API",
# "demo_addons": [
# ],
# "demo_addons_hidden": [
# ],
# "demo_url": "DEMO-URL",
# "demo_summary": "basic function and methods of API for openapi or XML-RPC",
# "demo_images": [
# "images/MAIN_IMAGE",
# ]
}
+8
View File
@@ -0,0 +1,8 @@
`1.0.1`
-------
- **Improvement:** Compatibility with python 3.9
`1.0.0`
-------
- **Init version**
+317
View File
@@ -0,0 +1,317 @@
==========
Base Api
==========
Usage
=====
*Methods intended for calling via API (e.g. OpenAPI or RPC)*:
search_or_create
----------------
*search_or_create(self, vals, active\_test=True)*
*– Purpose*:
- To resolve “race conditions”, comparing to separated searching
and creation, that can lead to record-duplication.
*– Input data*:
- `vals`-variable:
- fields-values as for *create*-method
- type of dictionary (not nested)
- e.g.
.. code-block::
vals = {
'name': 'John',
'Age': 25
}
- `active_test`-variable (for models that have field named `active`):
- flag to search only for *active* records
- type of *boolean*
- e.g.
.. code-block::
 active_test = False` # to also search in *in-active* records
*– Notes*:
- *many2one* fields in `vals`:
- type of integer
- e.g.
.. code-block::
vals = {
'company_id': 1
}
- *x2many* fields in `vals`:
- ignored for searching
- type of list of *tuples*
- e.g.
.. code-block::
vals = {
'name': John',
'children_ids': [(4,4,0),
(4,5,0)]
}
- For more information look `here <https://odoo-development.readthedocs.io/en/latest/dev/py/x2many.html>`__
*– Example*:
.. code-block::
-> # Searching for existing record
-> vals = {'company_id': 1 }
-> res_partner_object.search_or_create(vals)
(False, [22, 1, 8, 7, 3])
-> # Creating record (with many2one field)
-> vals = { 'name': 'John Doe Neo', 'company_id': 1 }
-> res_partner_object.search_or_create(vals)
(True, [78])
-> # Creating record (for x2many-fields)
-> vals = { 'name': 'Albert Bubis', 'child_ids': [(4, 11, 0), (4, 5, 0)] }
-> res_partner_object.search_or_create(vals)
(True, [79])
*– Algorithm*:
1. Creates *domain* out of `vals` for searching
2. Searches for records satisfiy *domain* constraints (`is_new = False`)
3. If no record was found:
- then it creates one with `vals`
- sets *True* to `is_new`
4. Returns two variables:
- `is_new` - *boolean*: Shows if record was created or not
- `ids` - list of records, that were found, or id of created
one
search_read_nested
------------------
*search_read_nested(self, domain=None, fields=None, offset=0, limit=None, order=None, delimeter='/')*
*– Purpose*:
- Simplifies reading data to one request;
- Comparing to default **search\_read**:
- ``fields`` can be nested, so the method will return list of
record-dictionaries with nested fields from related models (via
*x2many*, *many2one*). Nested fields are specified as slash-separated
sequence, for example:
.. code-block::
fields = [
'company_id/id',
'company_id/name'
]
*– Input data*:
- `domain`-variable:
- list of statements for searching, as for usual
*search*-method
- type of list of *tuples*
- e.g. 
.. code-block::
domain = [
('name', '=', 'John'),
('Age','>','10')
]
- `fields`-variable:
- fields to be read from founded records (including nested
fields via dot-notation)
- list of *strings*
- e.g. if ``author_id``, ``edition_ids`` are many2one and many2many
fields, then the variable can be specified as following:
.. code-block::
fields = [
'book_name',
'author_id/id',
'author_id/name',
`edition_ids/id`,
`edition_ids/year`
]
- `offset`-variable:
- number of records to ignore
- type of *integer*
- e.g. ``offset = 2`` # will ignore two first-founded records
- `limit`-variable:
- number of founded records to show
- type of
- e.g. ``limit = 3`` # will show three first-founded records
- `order`-variable:
- criteria of sorting founded records
- type of *string*
- e.g. ``order = 'name desc'`` # will sort records descending by ‘name’
- `count`-variable:
- flag to return number of founded records, instead of records
itself
- type of *boolean*
- e.g. ``count = True``
- `delimeter`-variable:
- char that divide nesting in field
- type of *char*
- e.g. ``company_id/country_id/name # delimeter='/'
*– Notes*:
- for *many2one* fields the method returns a dictionary with
nested fields
- for *x2many* fields the method returns list of
record-dictionaries with nested fields
*– Example*
.. code-block::
-> search_domain = [('company_id.category', '=', 'Supermarket')]
-> show_fields = [ 'name', 'company_id/id', 'company_id/name', 'company_id/website', 'country_id/id', 'child_ids/name', 'child_ids/id' ]
-> res_partner_object.search_read_nested(domain=search_domain, fields=show_fields, '.')
[
{
'name': 'Partner #1',
'company_id': {
'id': 1,
'name': 'Supermarket for me',
'website': 'http://superfood.com'
},
'country_id': { 'id':102' },
'child_ids': [
{
'id': 1,
'name': Child #1,
}, {
'id': 2,
'name': Child #2,
}
]
},
...,
{
'name': 'Partner #37',
'company_id': {
'id': 25,
'name': 'Supermarket in Eternity',
'website': 'http://giantbroccoly.com'
},
'country_id': { 'id': 103 }
'child_ids': []
}
]
*– Algorithm*:
1. Searches for records that satisfy `domain`
2. Returns list of dictionaries with fields specified in `fields`
create_or_update_by_external_id
-------------------------------
*create_or_update_by_external_id(self, vals)*
*– Purpose*:
- work with model (create or update values) by custom (external)
identification
*– Input data*:
- `vals`-variable:
- type of *dictionary* as for *create*-method
- Must contain `id` field type of *string*
- e.g.
.. code-block::
 vals = {
'id': 'ext.id_1',
'name: 'John',
'age': 37,
'job_id': 'ext.work_1',
'child_ids' : [
(4, 'ext.child_1', 0),
(4, 'ext.child_2', 0)
]
}`
*– Notes*:
- for *x2x*-fields `id` might be *string* (external id)
- for *x2many*-fields use *tuples* `this <https://odoo-development.readthedocs.io/en/latest/dev/py/x2many.html>`__,
- If `id` of *x2x* fields are not found, it will return error
(*raise Exception*). In order to avoid this, call the function
for the models of this fields
- Work of function based on *External Identifiers* (**ir.model.data** )
*– Example*
.. code-block::
-> # Create non-existed record
-> vals = {
'id': 'ext.id_5',
'name': 'John',
'customer_id': 'ext.id_3',
'child_ids': [(4, 'ext.id_child_5, 0), (4, 5, 0)]
}
-> sale_order_object.create_or_update_by_external_id(vals)
(True, 38)
-> # Update existing record
-> vals = {
'id': 'ext.id_5',
'customer_id': 'ext.id_5',
'child_ids': [(4, 'ext.id_child_4', 0)]
}
-> sale_order_object.create_or_update_by_external_id(vals)
(False, 38)
*– Algorithm*:
- Searches for record by its external id (`id` in `vals`) through
*self.env.ref*-function
- If no record was found:
- then it creates one with requested values (`vals`)
- register `id` of `vals` in **ir.model.data**
- sets *True* to `is_new`
- Returns two variables:
- `is_new` - *True* or *False*: if record was created or not
- `id` (inner) of updated or created record
+1
View File
@@ -0,0 +1 @@
from . import pinguin
+359
View File
@@ -0,0 +1,359 @@
# Copyright 2018, XOE Solutions
# Copyright 2018-2019 Rafis Bikbov <https://it-projects.info/team/bikbov>
# Copyright 2019 Yan Chirino <https://xoe.solutions/>
# Copyright 2019-2020 Anvar Kildebekov <https://it-projects.info/team/fedoranvar>
# Copyright 2020 Ivan Yelizariev
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
# pyling: disable=redefined-builtin
import collections
import collections.abc
import datetime
import six
import werkzeug.wrappers
from psycopg2.extensions import ISOLATION_LEVEL_READ_COMMITTED
import odoo
from odoo.http import request
try:
import simplejson as json
except ImportError:
import json
# 4xx Client Errors
CODE__obj_not_found = (
404,
"Object not found",
"This object is not available on this instance.",
)
# 5xx Server errors
CODE__invalid_spec = (
501,
"Invalid Field Spec",
"The field spec supplied is not valid.",
)
def error_response(status, error, error_descrip):
"""Error responses wrapper.
:param int status: The error code.
:param str error: The error summary.
:param str error_descrip: The error description.
:returns: The werkzeug `response object`_.
:rtype: werkzeug.wrappers.Response
.. _response object:
http://werkzeug.pocoo.org/docs/0.14/wrappers/#module-werkzeug.wrappers
"""
return werkzeug.wrappers.Response(
status=status,
content_type="application/json; charset=utf-8",
response=json.dumps({"error": error, "error_descrip": error_descrip}),
)
def validate_extra_field(field):
"""Validates extra fields on the fly.
:param str field: The name of the field.
:returns: None, if validated, otherwise raises.
:rtype: None
:raise: werkzeug.exceptions.HTTPException if field is invalid.
"""
if not isinstance(field, str):
return werkzeug.exceptions.HTTPException(
response=error_response(*CODE__invalid_spec)
)
def validate_spec(model, spec):
"""Validates a spec for a given model.
:param object model: (:obj:`Model`) The model against which to validate.
:param list spec: The spec to validate.
:returns: None, if validated, otherwise raises.
:rtype: None
:raise: Exception:
* if the tuple representing the field does not have length 2.
* if the second part of the tuple representing the field is not a list or tuple.
* if if a tuple representing a field consists of two parts, but the first part is not a relative field.
* if if the second part of the tuple representing the field is of type tuple, but the field is the ratio 2many.
* if if the field is neither a string nor a tuple.
"""
self = model
for field in spec:
if isinstance(field, tuple):
# Syntax checks
if len(field) != 2:
raise Exception(
"Tuples representing fields must have length 2. (%r)" % field
)
if not isinstance(field[1], (tuple, list)):
raise Exception(
"""Tuples representing fields must have a tuple wrapped in
a list or a bare tuple as it's second item. (%r)"""
% field
)
# Validity checks
fld = self._fields[field[0]]
if not fld.relational:
raise Exception(
"Tuples representing fields can only specify relational fields. (%r)"
% field
)
if isinstance(field[1], tuple) and fld.type in ["one2many", "many2many"]:
raise Exception(
"Specification of a 2many record cannot be a bare tuple. (%r)"
% field
)
elif not isinstance(field, six.string_types):
raise Exception(
"Fields are represented by either a strings or tuples. Found: %r"
% type(field)
)
def update(d, u):
"""Update value of a nested dictionary of varying depth.
:param dict d: Dictionary to update.
:param dict u: Dictionary with updates.
:returns: Merged dictionary.
:rtype: dict
"""
for k, v in u.items():
if isinstance(v, collections.abc.Mapping):
d[k] = update(d.get(k, collections.OrderedDict([])), v)
else:
d[k] = v
return d
# Transform string fields to dictionary
def transform_strfields_to_dict(fields_list, delim="/"):
"""Transform string fields to dictionary.
Example:
for ['name', 'email', 'bank_ids/bank_id/id', 'bank_ids/bank_name', 'bank_ids/id']
the result will be the next dictionary
{
'name': None,
'email': None
'bank_ids': {
'bank_name': None,
'bank_id': {
'id': None
}
},
}
:param list fields_list: The list of string fields.
:returns: The dict of transformed fields.
:rtype: dict
"""
dct = {}
for field in fields_list:
parts = field.split(delim)
data = None
for part in parts[::-1]:
if part == ".id":
part = "id"
data = {part: data}
update(dct, data)
return dct
def transform_dictfields_to_list_of_tuples(record, dct, ENV=False):
"""Transform fields dictionary to list.
for {
'name': None,
'email': None
'bank_ids': {
'bank_name': None,
'bank_id': {
'id': None
}
},
}
the result will be
['name', 'email', ('bank_ids', ['bank_name', ('bank_id', ('id',))])]
:param odoo.models.Model record: The model object.
:param dict dct: The dictionary.
:returns: The list of transformed fields.
:rtype: list
"""
fields_with_meta = {
k: meta for k, meta in record.fields_get().items() if k in dct.keys()
}
result = {}
for key, value in dct.items():
if isinstance(value, dict):
model_obj = get_model_for_read(
fields_with_meta[key]["relation"], ENV)
inner_result = transform_dictfields_to_list_of_tuples(
model_obj, value, ENV)
is_2many = fields_with_meta[key]["type"].endswith("2many")
result[key] = list(
inner_result) if is_2many else tuple(inner_result)
else:
result[key] = value
return [(key, value) if value else key for key, value in result.items()]
#######################
# Pinguin ORM Wrapper #
#######################
# List of dicts from model
def get_dictlist_from_model(model, spec, **kwargs):
"""Fetch dictionary from one record according to spec.
:param str model: The model against which to validate.
:param tuple spec: The spec to validate.
:param dict kwargs: Keyword arguments.
:param list kwargs['domain']: (optional). The domain to filter on.
:param int kwargs['offset']: (optional). The offset of the queried records.
:param int kwargs['limit']: (optional). The limit to query.
:param str kwargs['order']: (optional). The postgres order string.
:param tuple kwargs['include_fields']: (optional). The extra fields.
This parameter is not implemented on higher level code in order
to serve as a soft ACL implementation on top of the framework's
own ACL.
:param tuple kwargs['exclude_fields']: (optional). The excluded fields.
:param char kwargs['delimeter']: delimeter of nested fields.
:param object kwargs['env']: Model's environment.
:returns: The list of python dictionaries of the requested values.
:rtype: list
"""
domain = kwargs.get("domain", [])
offset = kwargs.get("offset", 0)
limit = kwargs.get("limit")
order = kwargs.get("order")
include_fields = kwargs.get(
"include_fields", ()
) # Not actually implemented on higher level (ACL!)
exclude_fields = kwargs.get("exclude_fields", ())
delim = kwargs.get("delimeter", "/")
ENV = kwargs.get("env", False)
model_obj = get_model_for_read(model, ENV)
records = model_obj.sudo().search(domain, offset=offset, limit=limit, order=order)
# Do some optimization for subfields
_prefetch = {}
for field in spec:
if isinstance(field, str):
continue
_fld = records._fields[field[0]]
if _fld.relational:
_prefetch[_fld.comodel] = records.mapped(field[0]).ids
for mod, ids in _prefetch.items():
get_model_for_read(mod, ENV).browse(ids).read()
result = []
for record in records:
result += [
get_dict_from_record(
record, spec, include_fields, exclude_fields, ENV, delim
)
]
return result
# Get a model with special context
def get_model_for_read(model, ENV=False):
"""Fetch a model object from the environment optimized for read.
Postgres serialization levels are changed to allow parallel read queries.
To increase the overall efficiency, as it is unlikely this API will be used
as a mass transactional interface. Rather we assume sequential and structured
integration workflows.
:param str model: The model to retrieve from the environment.
:param object env: Environment
:returns: the framework model if exist, otherwise raises.
:rtype: odoo.models.Model
:raise: werkzeug.exceptions.HTTPException if the model not found in env.
"""
if ENV:
return ENV[model]
cr, uid = request.cr, request.session.uid
test_mode = request.registry.test_cr
if not test_mode:
# Permit parallel query execution on read
# Contrary to ISOLATION_LEVEL_SERIALIZABLE as per Odoo Standard
cr._cnx.set_isolation_level(ISOLATION_LEVEL_READ_COMMITTED)
try:
return request.env(cr, uid)[model]
except KeyError:
err = list(CODE__obj_not_found)
err[2] = 'The "%s" model is not available on this instance.' % model
raise werkzeug.exceptions.HTTPException(response=error_response(*err))
# Python > 3.5
# def get_dict_from_record(record, spec: tuple, include_fields: tuple, exclude_fields: tuple):
# Extract nested values from a record
def get_dict_from_record(
record, spec, include_fields, exclude_fields, ENV=False, delim="/"
):
"""Generates nested python dict representing one record.
Going down to the record level, as the framework does not support nested
data queries natively as they are typical for a REST API.
:param odoo.models.Model record: The singleton record to load.
:param tuple spec: The field spec to load.
:param tuple include_fields: The extra fields.
:param tuple exclude_fields: The excluded fields.
:returns: The python dictionary representing the record according to the field spec.
:rtype collections.OrderedDict
"""
map(validate_extra_field, include_fields + exclude_fields)
result = collections.OrderedDict([])
_spec = [fld for fld in spec if fld not in exclude_fields] + \
list(include_fields)
if list(filter(lambda x: isinstance(x, six.string_types) and delim in x, _spec)):
_spec = transform_dictfields_to_list_of_tuples(
record, transform_strfields_to_dict(_spec, delim), ENV
)
validate_spec(record, _spec)
for field in _spec:
if isinstance(field, tuple):
# It's a 2many (or a 2one specified as a list)
if isinstance(field[1], list):
result[field[0]] = []
for rec in record[field[0]]:
result[field[0]] += [
get_dict_from_record(rec, field[1], (), (), ENV, delim)
]
# It's a 2one
if isinstance(field[1], tuple):
result[field[0]] = get_dict_from_record(
record[field[0]], field[1], (), (), ENV, delim
)
# Normal field, or unspecified relational
elif isinstance(field, six.string_types):
if not hasattr(record, field):
raise odoo.exceptions.ValidationError(
odoo._('The model "%s" has no such field: "%s".')
% (record._name, field)
)
# result[field] = getattr(record, field)
if isinstance(record[field], datetime.date):
value = record[field].strftime("%Y-%m-%d %H:%M:%S")
else:
value = record[field]
result[field] = value
fld = record._fields[field]
if fld.relational:
if fld.type.endswith("2one"):
result[field] = value.id
elif fld.type.endswith("2many"):
result[field] = value.ids
elif (value is False or value is None) and fld.type != "boolean":
# string field cannot be false in response json
result[field] = ""
return result
+3
View File
@@ -0,0 +1,3 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from . import base
+106
View File
@@ -0,0 +1,106 @@
# Copyright 2019 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2019 Anvar Kildebekov <https://it-projects.info/team/fedoranvar>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import api, models
from ..lib import pinguin
PREFIX = "__base_api__"
class Base(models.AbstractModel):
_inherit = "base"
@api.model
def search_or_create(self, vals, active_test=True):
domain = [
(k, "=", v)
for k, v in vals.items()
if not self._fields.get(k).type.endswith("2many")
]
records = self.with_context(active_test=active_test).search(domain)
is_new = False
if not records:
is_new = True
records = self.create(vals)
return (is_new, records.ids)
@api.model
def search_read_nested(
self, domain=None, fields=None, offset=0, limit=None, order=None, delimeter="/"
):
result = pinguin.get_dictlist_from_model(
self._name,
tuple(fields),
domain=domain,
offset=offset,
limit=limit,
order=order,
env=self.env,
delimeter=delimeter,
)
return result
@api.model
def create_or_update_by_external_id(self, vals):
ext_id = vals.get("id")
is_new = False
imd_env = self.env["ir.model.data"]
# if external id not defined
if not isinstance(ext_id, str):
raise ValueError('"id" field must be type of "string"')
# if x2x fields values are exist
fields_2many = []
def convert_external_2_inner_id(ext_id):
try:
result = imd_env.xmlid_lookup(PREFIX + "." + ext_id)[2]
except ValueError:
raise ValueError(
"No object with external id in field {}: {}".format(field, ext_id)
)
return result
for field in vals:
# for many2one fields
if self._fields[field].type == "many2one" and isinstance(vals[field], str):
vals[field] = convert_external_2_inner_id(vals.get(field))
elif self._fields[field].type.endswith("2many"):
fields_2many.append(field)
# for x2many fields
for field in fields_2many:
for index, tuple_record in enumerate(vals[field]):
list_record = list(tuple_record)
if list_record[0] in [1, 2, 3, 4] and isinstance(list_record[1], str):
list_record[1] = convert_external_2_inner_id(list_record[1])
elif list_record[0] == 6:
for record_for_replace in list_record[2]:
if isinstance(record_for_replace, str):
record_for_replace = convert_external_2_inner_id(
record_for_replace
)
vals[field][index] = tuple(list_record)
# If external id exists...
try:
inner_id = imd_env.xmlid_lookup(PREFIX + "." + ext_id)[2]
# No: Create record and register external_key
except ValueError:
is_new = True
inner_id = self.create(vals).id
imd_env.create(
{
"name": vals.get("id"),
"model": self._name,
"module": PREFIX,
"res_id": inner_id,
}
)
else:
# Yes: Write changes to record
self.browse(inner_id).write(vals)
return (is_new, inner_id)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,96 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan" style="color:#875A7B;">Base API</h2>
<h3 class="oe_slogan">Improve your API experience</h3>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<div class="alert alert-info oe_mt32" style="padding:0.3em 0.6em; font-size: 150%;">
<i class="fa fa-hand-o-right"></i><b> Key features: </b>
<ul class="list-unstyled">
<li>
<i class="fa fa-check-square-o text-primary"></i>
Eliminate race hazard on records creation
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
Nested data receiving in a single API request
</li>
<li>
<i class="fa fa-check-square-o text-primary"></i>
Painless work with <em>External Identifiers</em>
</li>
</ul>
</div>
</div>
</div>
</section>
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span8">
<h2>Need our service?</h2>
<p class="oe_mt32">Contact us by <a href="mailto:apps@it-projects.info">email</a> or fill out <a
href="https://www.it-projects.info/page/website.contactus " target="_blank">request form</a></p>
<ul>
<li><a href="mailto:apps@it-projects.info">apps@it-projects.info <i class="fa fa-envelope-o"></i></a>
</li>
<li><a href="https://www.it-projects.info/page/website.contactus "
target="_blank">https://www.it-projects.info/page/website.contactus <i
class="fa fa-list-alt"></i></a></li>
<li><a href="https://m.me/itprojectsllc" target="_blank">https://m.me/itprojectsllc <i
class="fa fa-facebook-square"></i></a></li>
<li>skype@it-projects.info <i class="fa fa-skype"></i></li>
</ul>
</div>
<div class="oe_span4">
<div class="stamp" style="width:200px;">
<div style="margin-top: 15px;
position: relative;
font-family:'Vollkorn', serif;
font-size: 16px;
line-height: 25px;
text-transform: uppercase;
font-weight: bold;
color: #75526b;
border: 3px dashed #75526b;
float: left;
padding: 4px 12px;
-webkit-transform: rotate(-6deg);
-o-transform: rotate(-6deg);
-moz-transform: rotate(-6deg);
-ms-transform: rotate(-6deg);">
Tested on Odoo<br />13.0 community
</div>
<!--<div style="margin-top: 15px;
position: relative;
font-family:'Vollkorn', serif;
font-size: 16px;
line-height: 25px;
text-transform: uppercase;
font-weight: bold;
color: #75526b;
border: 3px dashed #75526b;
float: left;
padding: 4px 12px;
-webkit-transform: rotate(9deg);
-o-transform: rotate(9deg);
-moz-transform: rotate(9deg);
-ms-transform: rotate(9deg);">
Tested on Odoo<br/>13.0 enterprise
</div>-->
</div>
</div>
</div>
</section>
+3
View File
@@ -0,0 +1,3 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from . import test_base
+246
View File
@@ -0,0 +1,246 @@
# Copyright 2019 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2019 Anvar Kildebekov <https://it-projects.info/team/fedoranvar>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
prefix = "__base_api__."
@tagged("post_install", "at_install")
class TestBase(TransactionCase):
def test_search_or_create(self):
# define test variables
partner_obj = self.env["res.partner"]
company_obj = self.env["res.company"]
t_name = "test_search_or_create"
#
# Test #1: Record creation
#
t_vals = {"name": t_name}
partner_obj.search([("name", "=", t_name)]).unlink()
is_new, record_ids = partner_obj.search_or_create(t_vals)
record = partner_obj.browse(record_ids)
# (1) record was created
# (2) record have field's value that was requested
self.assertTrue(is_new)
self.assertEqual(record.name, t_name)
#
# Test #2: Record searching
#
is_new, record_ids2 = partner_obj.search_or_create(t_vals)
record = partner_obj.browse(record_ids2)
# (1) record have been founded (not created)
# (2) this the same record as in the Test #1
self.assertFalse(is_new)
self.assertEqual(record_ids[0], record_ids2[0])
#
# Test #3: Record creation (x2x fields)
#
t_child_1 = partner_obj.create({"name": "TestChild1"})
t_child_2 = partner_obj.create({"name": "TestChild2"})
t_company = company_obj.create({"name": "TestCompany"})
t_vals = {
"name": "TestParent",
"child_ids": [(4, t_child_1.id, 0), (4, t_child_2.id, 0)],
"company_id": t_company.id,
}
is_new, record_ids3 = partner_obj.search_or_create(t_vals)
record = partner_obj.browse(record_ids3)
# (1) record was created
# (2) record have x2x field's values that were requested?
self.assertTrue(is_new)
self.assertEqual([t_child_1.id, t_child_2.id], record.child_ids.ids)
self.assertEqual(record.company_id.id, t_company.id)
#
# Test #4: Record searching (x2many fields are ignored)
#
is_new, record_ids4 = partner_obj.search_or_create(t_vals)
# (1) record have been founded (not created)
# (2) this is the same record as in the Test #3
self.assertFalse(is_new)
self.assertEqual(record_ids3[0], record_ids4[0])
#
# unlink test objects
#
partner_obj.browse(record_ids[0]).unlink()
partner_obj.browse(record_ids2[0]).unlink()
partner_obj.browse(record_ids3[0]).unlink()
partner_obj.browse(record_ids4[0]).unlink()
def test_search_read_nested(self):
# Define test variables
partner_obj = self.env["res.partner"]
country_obj = self.env["res.country"]
company_obj = self.env["res.company"]
category_obj = self.env["res.partner.category"]
t_country_1 = country_obj.create({"name": "TestCountry1"})
t_country_2 = country_obj.create({"name": "TestCountry2"})
t_company_1 = company_obj.create(
{"name": "TestCompany1", "country_id": t_country_1.id}
)
t_company_2 = company_obj.create(
{"name": "TestCompany2", "country_id": t_country_2.id}
)
t_category_1 = category_obj.create({"name": "TestCategory1"})
t_category_2 = category_obj.create({"name": "TestCategory2"})
t_category_3 = category_obj.create({"name": "TestCategory3"})
t_category_4 = category_obj.create({"name": "TestCategory4"})
t_partner_1 = partner_obj.create(
{
"name": "TestPartner1",
"company_id": t_company_1.id,
"category_id": [(4, t_category_1.id, 0), (4, t_category_2.id, 0)],
"street": "TestStreet",
}
)
t_partner_2 = partner_obj.create(
{
"name": "TestPartner2",
"company_id": t_company_2.id,
"category_id": [(4, t_category_3.id, 0), (4, t_category_4.id, 0)],
"street": "TestStreet",
}
)
correct_result = [
{
"name": t_partner_1.name,
"category_id": [
{"id": t_category_1.id, "name": t_category_1.name},
{"id": t_category_2.id, "name": t_category_2.name},
],
"company_id": {
"id": t_company_1.id,
"name": t_company_1.name,
"country_id": {"id": t_country_1.id, "name": t_country_1.name},
},
},
{
"name": t_partner_2.name,
"category_id": [
{"id": t_category_3.id, "name": t_category_3.name},
{"id": t_category_4.id, "name": t_category_4.name},
],
"company_id": {
"id": t_company_2.id,
"name": t_company_2.name,
"country_id": {"id": t_country_2.id, "name": t_country_2.name},
},
},
]
#
# Test 1: Record searching-reading
#
search_domain = [("street", "=", "TestStreet")]
show_fields = [
"name",
"category_id.id",
"category_id.name",
"company_id.id",
"company_id.name",
"company_id.country_id.name",
"company_id.country_id.id",
]
delimeter = "."
record_list = partner_obj.search_read_nested(
domain=search_domain, fields=show_fields, delimeter=delimeter
)
# (1) records has requested values
self.assertEqual(correct_result, record_list)
#
# Unlink test objects
#
t_partner_1.unlink()
t_partner_2.unlink()
t_company_1.write({"country_id": [(5, 0, 0)]})
t_company_2.write({"country_id": [(5, 0, 0)]})
t_company_1.unlink()
t_company_2.unlink()
t_category_1.unlink()
t_category_2.unlink()
t_category_3.unlink()
t_category_4.unlink()
t_country_1.unlink()
t_country_2.unlink()
def test_create_or_update_by_external_id(self):
partner_obj = self.env["res.partner"]
company_obj = self.env["res.company"]
t_company_ext_id = "ext.company_1"
t_child_1_ext_id = "ext.child_1"
t_child_2_ext_id = "ext.child_2"
#
# Test #0: Check correct creation of external id
#
t_company = company_obj.browse(
company_obj.create_or_update_by_external_id(
{"id": t_company_ext_id, "name": "TestCompany"}
)[1]
)
t_child_1 = partner_obj.browse(
partner_obj.create_or_update_by_external_id(
{"id": t_child_1_ext_id, "name": "TestChild1"}
)[1]
)
t_child_2 = partner_obj.browse(
partner_obj.create_or_update_by_external_id(
{"id": t_child_2_ext_id, "name": "TestChild2"}
)[1]
)
# (1) Check field value (correctness of creation)
# (2) Check field value (correctness of creation)
# (3) Check field value (correctness of creation)
self.assertEqual(
t_company.get_external_id()[t_company.id].split(".", 1)[1], t_company_ext_id
)
self.assertEqual(
t_child_1.get_external_id()[t_child_1.id].split(".", 1)[1], t_child_1_ext_id
)
self.assertEqual(
t_child_2.get_external_id()[t_child_2.id].split(".", 1)[1], t_child_2_ext_id
)
#
# Test #1: Error : "External ID not defined"
#
t_vals = {
"name": "John",
"child_ids": [(4, t_child_1_ext_id, 0), (4, t_child_2_ext_id, 0)],
"company_id": t_company_ext_id,
}
with self.assertRaises(ValueError):
partner_obj.create_or_update_by_external_id(t_vals)
#
# Test #2: Record creation
#
t_vals["id"] = "ext.partner_1"
is_new, record_id2 = partner_obj.create_or_update_by_external_id(t_vals)
record = partner_obj.browse(record_id2)
# (1) record was created
# (2) record have requested external id
# (3) record have one2many-field's value that was requested
# (4) record have many2one-field's value that was requested
self.assertTrue(is_new)
self.assertEqual(record.get_external_id()[record.id], prefix + "ext.partner_1")
self.assertEqual(record.child_ids.ids, [t_child_1.id, t_child_2.id])
self.assertEqual(record.company_id, t_company)
#
# Test #3: Record update
#
t_vals = {"id": "ext.partner_1", "child_ids": [(3, t_child_2_ext_id, 0)]}
is_new, record_id3 = partner_obj.create_or_update_by_external_id(t_vals)
record = partner_obj.browse(record_id3)
# (1) record was updated
# (2) this is the same record (by id) that was created in Test#1
# (3) record have one2many field's value that was requested
self.assertFalse(is_new)
self.assertEqual(record_id2, record_id3)
self.assertEqual(record.child_ids.ids, [t_child_1.id])
#
# Unlink test objects
#
partner_obj.browse(record_id2).unlink()
partner_obj.browse(record_id3).unlink()
t_company.unlink()
t_child_1.unlink()
t_child_2.unlink()