BinaryONe
commited on
Commit
·
f9dacfc
1
Parent(s):
b77888d
initial Commit
Browse files- .gitignore +53 -0
- Dockerfile +17 -0
- Dockerfile_old +60 -0
- odoo.conf +16 -0
- requirements.txt +92 -0
- rest-api/LICENSE +21 -0
- rest-api/README.md +598 -0
- rest-api/__init__.py +4 -0
- rest-api/__manifest__.py +42 -0
- rest-api/controllers/__init__.py +3 -0
- rest-api/controllers/controllers.py +462 -0
- rest-api/controllers/exceptions.py +2 -0
- rest-api/controllers/parser.py +171 -0
- rest-api/controllers/serializers.py +151 -0
- rest-api/demo/demo.xml +5 -0
- rest-api/models/__init__.py +3 -0
- rest-api/models/models.py +3 -0
- rest-api/security/ir.model.access.csv +1 -0
- rest-api/views/templates.xml +5 -0
- rest-api/views/views.xml +5 -0
.gitignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# sphinx build directories
|
| 2 |
+
_build/
|
| 3 |
+
|
| 4 |
+
# dotfiles
|
| 5 |
+
.*
|
| 6 |
+
!.gitignore
|
| 7 |
+
!.github
|
| 8 |
+
!.mailmap
|
| 9 |
+
# compiled python files
|
| 10 |
+
*.py[co]
|
| 11 |
+
*.pyc
|
| 12 |
+
__pycache__/
|
| 13 |
+
# setup.py egg_info
|
| 14 |
+
*.egg-info
|
| 15 |
+
# emacs backup files
|
| 16 |
+
*~
|
| 17 |
+
# hg stuff
|
| 18 |
+
*.orig
|
| 19 |
+
status
|
| 20 |
+
# odoo filestore
|
| 21 |
+
#odoo/filestore
|
| 22 |
+
# maintenance migration scripts
|
| 23 |
+
#odoo/addons/base/maintenance
|
| 24 |
+
# window installation config file
|
| 25 |
+
#odoo.conf
|
| 26 |
+
|
| 27 |
+
# generated for windows installer?
|
| 28 |
+
install/win32/*.bat
|
| 29 |
+
install/win32/meta.py
|
| 30 |
+
|
| 31 |
+
# needed only when building for win32
|
| 32 |
+
setup/win32/static/less/
|
| 33 |
+
setup/win32/static/wkhtmltopdf/
|
| 34 |
+
setup/win32/static/postgresql*.exe
|
| 35 |
+
|
| 36 |
+
# js tooling
|
| 37 |
+
node_modules
|
| 38 |
+
jsconfig.json
|
| 39 |
+
tsconfig.json
|
| 40 |
+
package-lock.json
|
| 41 |
+
package.json
|
| 42 |
+
.husky
|
| 43 |
+
|
| 44 |
+
# various virtualenv
|
| 45 |
+
/bin/
|
| 46 |
+
/build/
|
| 47 |
+
/dist/
|
| 48 |
+
/include/
|
| 49 |
+
/lib/
|
| 50 |
+
/man/
|
| 51 |
+
/share/
|
| 52 |
+
/src/
|
| 53 |
+
.venv/
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Odoo 18 image
|
| 2 |
+
FROM odoo:18.0
|
| 3 |
+
|
| 4 |
+
# Set environment vars
|
| 5 |
+
ENV ADDONS_PATH=/mnt/rest-api
|
| 6 |
+
|
| 7 |
+
# Copy config
|
| 8 |
+
COPY ./odoo.conf /etc/odoo/odoo.conf
|
| 9 |
+
|
| 10 |
+
# Copy your custom addon
|
| 11 |
+
COPY ./rest-api /mnt/rest-api
|
| 12 |
+
|
| 13 |
+
# Expose Odoo web port
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
|
| 16 |
+
# Start Odoo with your config
|
| 17 |
+
CMD ["odoo", "-c", "/etc/odoo/odoo.conf"]
|
Dockerfile_old
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python 3.11 base image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
# Create a non-root user
|
| 6 |
+
RUN useradd -ms /bin/bash admin
|
| 7 |
+
|
| 8 |
+
# Install dependencies and Node.js (18.x)
|
| 9 |
+
RUN apt-get update && apt-get upgrade -y && \
|
| 10 |
+
apt-get install -y --no-install-recommends \
|
| 11 |
+
ksh \
|
| 12 |
+
curl \
|
| 13 |
+
nginx \
|
| 14 |
+
gnupg \
|
| 15 |
+
certbot \
|
| 16 |
+
wkhtmltopdf \
|
| 17 |
+
build-essential \
|
| 18 |
+
libpq-dev \
|
| 19 |
+
libssl-dev \
|
| 20 |
+
libffi-dev \
|
| 21 |
+
python3-dev \
|
| 22 |
+
libldap2-dev \
|
| 23 |
+
libsasl2-dev \
|
| 24 |
+
python3-certbot-nginx && \
|
| 25 |
+
# Add Node.js 18.x from NodeSource
|
| 26 |
+
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
|
| 27 |
+
apt-get install -y nodejs && \
|
| 28 |
+
apt-get clean && \
|
| 29 |
+
rm -rf /var/lib/apt/lists/*
|
| 30 |
+
|
| 31 |
+
# Print versions (optional debug)
|
| 32 |
+
RUN node -v && npm -v && python3 --version && pip3 --version
|
| 33 |
+
|
| 34 |
+
# Copy application files
|
| 35 |
+
#COPY . /app
|
| 36 |
+
COPY odoo.conf requirements.txt /app/
|
| 37 |
+
|
| 38 |
+
# Copy Angular build files to Nginx web directory
|
| 39 |
+
ADD ./odoo.tar /app/odoo
|
| 40 |
+
|
| 41 |
+
# Set directory permissions
|
| 42 |
+
RUN chmod 777 /app
|
| 43 |
+
|
| 44 |
+
# Set the working directory to /app
|
| 45 |
+
WORKDIR /app
|
| 46 |
+
|
| 47 |
+
# Set ownership and permissions for the app directory
|
| 48 |
+
RUN chown -R admin:admin /app && chmod -R 777 /app/*
|
| 49 |
+
|
| 50 |
+
# Switch to the non-root user for better security
|
| 51 |
+
USER admin
|
| 52 |
+
|
| 53 |
+
# Expose the port the application will run on
|
| 54 |
+
EXPOSE 7860
|
| 55 |
+
|
| 56 |
+
# Install Python dependencies
|
| 57 |
+
RUN pip3 install --no-cache-dir --upgrade -r requirements.txt
|
| 58 |
+
|
| 59 |
+
# Run the Odoo server
|
| 60 |
+
CMD ["python3", "odoo/odoo-bin", "-c", "/app/odoo.conf"]
|
odoo.conf
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[options]
|
| 2 |
+
; Is This The Password That Allows Database Operations:
|
| 3 |
+
admin_passwd = admin
|
| 4 |
+
db_host = ep-withered-bar-a1n6c8l2-pooler.ap-southeast-1.aws.neon.tech
|
| 5 |
+
db_port = 5432
|
| 6 |
+
db_name = odoo_db
|
| 7 |
+
db_user = odoo_admin
|
| 8 |
+
db_password = npg_2rj1lSdVhKqi
|
| 9 |
+
interface = 0.0.0.0
|
| 10 |
+
xmlrpc_interface = 0.0.0.0
|
| 11 |
+
port= 7860
|
| 12 |
+
http_port = 7860
|
| 13 |
+
xmlrpc_port = 7860
|
| 14 |
+
server_wide_modules = web, base
|
| 15 |
+
addons_path= /usr/lib/python3/dist-packages/odoo/addons
|
| 16 |
+
;addons_path = /app/odoo/odoo/addons
|
requirements.txt
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The officially supported versions of the following packages are their
|
| 2 |
+
#setuptools>=65.5.1
|
| 3 |
+
# python3-* equivalent distributed in Ubuntu 24.04 and Debian 12
|
| 4 |
+
asn1crypto==1.4.0 ; python_version < '3.11'
|
| 5 |
+
asn1crypto==1.5.1 ; python_version >= '3.11'
|
| 6 |
+
Babel==2.9.1 ; python_version < '3.11' # min version = 2.6.0 (Focal with security backports)
|
| 7 |
+
Babel==2.10.3 ; python_version >= '3.11'
|
| 8 |
+
cbor2==5.4.2 ; python_version < '3.12'
|
| 9 |
+
cbor2==5.6.2 ; python_version >= '3.12'
|
| 10 |
+
chardet==4.0.0 ; python_version < '3.11' # (Jammy)
|
| 11 |
+
chardet==5.2.0 ; python_version >= '3.11'
|
| 12 |
+
cryptography==3.4.8; python_version < '3.12' # incompatibility between pyopenssl 19.0.0 and cryptography>=37.0.0
|
| 13 |
+
cryptography==42.0.8 ; python_version >= '3.12' # (Noble) min 41.0.7, pinning 42.0.8 for security fixes
|
| 14 |
+
decorator==4.4.2 ; python_version < '3.11' # (Jammy)
|
| 15 |
+
decorator==5.1.1 ; python_version >= '3.11'
|
| 16 |
+
docutils==0.17 ; python_version < '3.11' # (Jammy)
|
| 17 |
+
docutils==0.20.1 ; python_version >= '3.11'
|
| 18 |
+
freezegun==1.1.0 ; python_version < '3.11' # (Jammy)
|
| 19 |
+
freezegun==1.2.1 ; python_version >= '3.11'
|
| 20 |
+
geoip2==2.9.0
|
| 21 |
+
gevent==21.8.0 ; sys_platform != 'win32' and python_version == '3.10' # (Jammy)
|
| 22 |
+
gevent==22.10.2; sys_platform != 'win32' and python_version > '3.10' and python_version < '3.12'
|
| 23 |
+
gevent==24.2.1 ; sys_platform != 'win32' and python_version >= '3.12' # (Noble)
|
| 24 |
+
greenlet==1.1.2 ; sys_platform != 'win32' and python_version == '3.10' # (Jammy)
|
| 25 |
+
greenlet==2.0.2 ; sys_platform != 'win32' and python_version > '3.10' and python_version < '3.12'
|
| 26 |
+
greenlet==3.0.3 ; sys_platform != 'win32' and python_version >= '3.12' # (Noble)
|
| 27 |
+
idna==2.10 ; python_version < '3.12' # requests 2.25.1 depends on idna<3 and >=2.5
|
| 28 |
+
idna==3.6 ; python_version >= '3.12'
|
| 29 |
+
Jinja2==3.0.3 ; python_version <= '3.10'
|
| 30 |
+
Jinja2==3.1.2 ; python_version > '3.10'
|
| 31 |
+
libsass==0.20.1 ; python_version < '3.11'
|
| 32 |
+
libsass==0.22.0 ; python_version >= '3.11' # (Noble) Mostly to have a wheel package
|
| 33 |
+
lxml==4.8.0 ; python_version <= '3.10'
|
| 34 |
+
lxml==4.9.3 ; python_version > '3.10' and python_version < '3.12' # min 4.9.2, pinning 4.9.3 because of missing wheels for darwin in 4.9.3
|
| 35 |
+
lxml==5.2.1; python_version >= '3.12' # (Noble - removed html clean)
|
| 36 |
+
lxml-html-clean; python_version >= '3.12' # (Noble - removed from lxml, unpinned for futur security patches)
|
| 37 |
+
MarkupSafe==2.0.1 ; python_version <= '3.10'
|
| 38 |
+
MarkupSafe==2.1.2 ; python_version > '3.10' and python_version < '3.12'
|
| 39 |
+
MarkupSafe==2.1.5 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
| 40 |
+
num2words==0.5.10 ; python_version < '3.12' # (Jammy / Bookworm)
|
| 41 |
+
num2words==0.5.13 ; python_version >= '3.12'
|
| 42 |
+
ofxparse==0.21
|
| 43 |
+
openpyxl==3.0.9 ; python_version < '3.12'
|
| 44 |
+
openpyxl==3.1.2 ; python_version >= '3.12'
|
| 45 |
+
passlib==1.7.4 # min version = 1.7.2 (Focal with security backports)
|
| 46 |
+
Pillow==9.0.1 ; python_version <= '3.10'
|
| 47 |
+
Pillow==9.4.0 ; python_version > '3.10' and python_version < '3.12'
|
| 48 |
+
Pillow==10.2.0 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
| 49 |
+
polib==1.1.1
|
| 50 |
+
psutil==5.9.0 ; python_version <= '3.10'
|
| 51 |
+
psutil==5.9.4 ; python_version > '3.10' and python_version < '3.12'
|
| 52 |
+
psutil==5.9.8 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
| 53 |
+
psycopg2==2.9.2 ; python_version == '3.10' # (Jammy)
|
| 54 |
+
psycopg2==2.9.5 ; python_version == '3.11'
|
| 55 |
+
psycopg2==2.9.9 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
| 56 |
+
pyopenssl==21.0.0 ; python_version < '3.12'
|
| 57 |
+
pyopenssl==24.1.0 ; python_version >= '3.12' # (Noble) min 23.2.0, pinned for compatibility with cryptography==42.0.8 and security patches
|
| 58 |
+
PyPDF2==1.26.0 ; python_version <= '3.10'
|
| 59 |
+
PyPDF2==2.12.1 ; python_version > '3.10'
|
| 60 |
+
pypiwin32 ; sys_platform == 'win32'
|
| 61 |
+
pyserial==3.5
|
| 62 |
+
python-dateutil==2.8.1 ; python_version < '3.11'
|
| 63 |
+
python-dateutil==2.8.2 ; python_version >= '3.11'
|
| 64 |
+
python-ldap==3.4.0 ; sys_platform != 'win32' and python_version < '3.12' # min version = 3.2.0 (Focal with security backports)
|
| 65 |
+
python-ldap==3.4.4 ; sys_platform != 'win32' and python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
| 66 |
+
python-stdnum==1.17 ; python_version < '3.11' # (jammy)
|
| 67 |
+
python-stdnum==1.19 ; python_version >= '3.11'
|
| 68 |
+
pytz # no version pinning to avoid OS perturbations
|
| 69 |
+
pyusb==1.2.1
|
| 70 |
+
qrcode==7.3.1 ; python_version < '3.11' # (jammy)
|
| 71 |
+
qrcode==7.4.2 ; python_version >= '3.11'
|
| 72 |
+
reportlab==3.6.8 ; python_version <= '3.10'
|
| 73 |
+
reportlab==3.6.12 ; python_version > '3.10' and python_version < '3.12'
|
| 74 |
+
reportlab==4.1.0 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
| 75 |
+
requests==2.25.1 ; python_version < '3.11' # versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal)
|
| 76 |
+
requests==2.31.0 ; python_version >= '3.11' # (Noble)
|
| 77 |
+
rjsmin==1.1.0 ; python_version < '3.11' # (jammy)
|
| 78 |
+
rjsmin==1.2.0 ; python_version >= '3.11'
|
| 79 |
+
rl-renderPM==4.0.3 ; sys_platform == 'win32' and python_version >= '3.12' # Needed by reportlab 4.1.0 but included in deb package
|
| 80 |
+
urllib3==1.26.5 ; python_version < '3.12' # indirect / min version = 1.25.8 (Focal with security backports)
|
| 81 |
+
urllib3==2.0.7 ; python_version >= '3.12' # (Noble) Compatibility with cryptography
|
| 82 |
+
vobject==0.9.6.1
|
| 83 |
+
Werkzeug==2.0.2 ; python_version <= '3.10'
|
| 84 |
+
Werkzeug==2.2.2 ; python_version > '3.10' and python_version < '3.12'
|
| 85 |
+
Werkzeug==3.0.1 ; python_version >= '3.12' # (Noble) Avoid deprecation warnings
|
| 86 |
+
xlrd==1.2.0 ; python_version < '3.12' # (jammy)
|
| 87 |
+
xlrd==2.0.1 ; python_version >= '3.12'
|
| 88 |
+
XlsxWriter==3.0.2 ; python_version < '3.12' # (jammy)
|
| 89 |
+
XlsxWriter==3.1.9 ; python_version >= '3.12'
|
| 90 |
+
xlwt==1.3.0
|
| 91 |
+
zeep==4.1.0 ; python_version < '3.11' # (jammy)
|
| 92 |
+
zeep==4.2.1 ; python_version >= '3.11'
|
rest-api/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2021 Yezy Ilomo
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
rest-api/README.md
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Odoo REST API
|
| 2 |
+
This is a module which expose Odoo as a REST API
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
## Installing
|
| 6 |
+
* Download this module and put it to your Odoo addons directory
|
| 7 |
+
* Install requirements with `pip install -r requirements.txt`
|
| 8 |
+
|
| 9 |
+
## Getting Started
|
| 10 |
+
|
| 11 |
+
### Authenticating users
|
| 12 |
+
Before making any request make sure you are authenticated. The route which is used to authenticate users is `/auth/`. Below is an example showing how to authenticate users.
|
| 13 |
+
```py
|
| 14 |
+
import json
|
| 15 |
+
import requests
|
| 16 |
+
import sys
|
| 17 |
+
|
| 18 |
+
AUTH_URL = 'http://localhost:8069/auth/'
|
| 19 |
+
|
| 20 |
+
headers = {'Content-type': 'application/json'}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Remember to configure default db on odoo configuration file(dbfilter = ^db_name$)
|
| 24 |
+
# Authentication credentials
|
| 25 |
+
data = {
|
| 26 |
+
'params': {
|
| 27 |
+
'login': '[email protected]',
|
| 28 |
+
'password': 'yor_password',
|
| 29 |
+
'db': 'your_db_name'
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
# Authenticate user
|
| 34 |
+
res = requests.post(
|
| 35 |
+
AUTH_URL,
|
| 36 |
+
data=json.dumps(data),
|
| 37 |
+
headers=headers
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Get response cookies
|
| 41 |
+
# This hold information for authenticated user
|
| 42 |
+
cookies = res.cookies
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Example 1
|
| 46 |
+
# Get users
|
| 47 |
+
USERS_URL = 'http://localhost:8069/api/res.users/'
|
| 48 |
+
|
| 49 |
+
# This will take time since it retrives all res.users fields
|
| 50 |
+
# You can use query param to fetch specific fields
|
| 51 |
+
|
| 52 |
+
res = requests.get(
|
| 53 |
+
USERS_URL,
|
| 54 |
+
cookies=cookies # Here we are sending cookies which holds info for authenticated user
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# This will be a very long response since it has many data
|
| 58 |
+
print(res.text)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# Example 2
|
| 62 |
+
# Get products(assuming you have products in you db)
|
| 63 |
+
# Here am using query param to fetch only product id and name(This will be faster)
|
| 64 |
+
USERS_URL = 'http://localhost:8069/api/product.product/'
|
| 65 |
+
|
| 66 |
+
# Use query param to fetch only id and name
|
| 67 |
+
params = {'query': '{id, name}'}
|
| 68 |
+
|
| 69 |
+
res = requests.get(
|
| 70 |
+
USERS_URL,
|
| 71 |
+
params=params,
|
| 72 |
+
cookies=cookies # Here we are sending cookies which holds info for authenticated user
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# This will be small since we've retrieved only id and name
|
| 76 |
+
print(res.text)
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
## Allowed HTTP methods
|
| 81 |
+
|
| 82 |
+
## 1. GET
|
| 83 |
+
|
| 84 |
+
### Model records:
|
| 85 |
+
|
| 86 |
+
`GET /api/{model}/`
|
| 87 |
+
#### Parameters
|
| 88 |
+
* **query (optional):**
|
| 89 |
+
|
| 90 |
+
This parameter is used to dynamically select fields to include on a response. For example if we want to select `id` and `name` fields from `res.users` model here is how we would do it.
|
| 91 |
+
|
| 92 |
+
`GET /api/res.users/?query={id, name}`
|
| 93 |
+
|
| 94 |
+
```js
|
| 95 |
+
{
|
| 96 |
+
"count": 2,
|
| 97 |
+
"prev": null,
|
| 98 |
+
"current": 1,
|
| 99 |
+
"next": null,
|
| 100 |
+
"total_pages": 1,
|
| 101 |
+
"result": [
|
| 102 |
+
{
|
| 103 |
+
"id": 2,
|
| 104 |
+
"name": "Administrator"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": 6,
|
| 108 |
+
"name": "Sailors Co Ltd"
|
| 109 |
+
}
|
| 110 |
+
]
|
| 111 |
+
}
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
For nested records, for example if we want to select `id`, `name` and `company_id` fields from `res.users` model, but under `company_id` we want to select `name` field only. here is how we would do it.
|
| 115 |
+
|
| 116 |
+
`GET /api/res.users/?query={id, name, company_id{name}}`
|
| 117 |
+
|
| 118 |
+
```js
|
| 119 |
+
{
|
| 120 |
+
"count": 2,
|
| 121 |
+
"prev": null,
|
| 122 |
+
"current": 1,
|
| 123 |
+
"next": null,
|
| 124 |
+
"total_pages": 1,
|
| 125 |
+
"result": [
|
| 126 |
+
{
|
| 127 |
+
"id": 2,
|
| 128 |
+
"name": "Administrator",
|
| 129 |
+
"company_id": {
|
| 130 |
+
"name": "Singo Africa"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": 6,
|
| 135 |
+
"name": "Sailors Co Ltd",
|
| 136 |
+
"company_id": {
|
| 137 |
+
"name": "Singo Africa"
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
]
|
| 141 |
+
}
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
For nested iterable records, for example if we want to select `id`, `name` and `related_products` fields from `product.template` model, but under `related_products` we want to select `name` field only. here is how we would do it.
|
| 145 |
+
|
| 146 |
+
`GET /api/product.template/?query={id, name, related_products{name}}`
|
| 147 |
+
|
| 148 |
+
```js
|
| 149 |
+
{
|
| 150 |
+
"count": 2,
|
| 151 |
+
"prev": null,
|
| 152 |
+
"current": 1,
|
| 153 |
+
"next": null,
|
| 154 |
+
"total_pages": 1,
|
| 155 |
+
"result": [
|
| 156 |
+
{
|
| 157 |
+
"id": 16,
|
| 158 |
+
"name": "Alaf Resincot Steel Roof-16",
|
| 159 |
+
"related_products": [
|
| 160 |
+
{"name": "Alloy Steel AISI 4140 Bright Bars - All 5.8 meter longs"},
|
| 161 |
+
{"name": "Test product"}
|
| 162 |
+
]
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"id": 18,
|
| 166 |
+
"name": "Alaf Resincot Steel Roof-43",
|
| 167 |
+
"related_products": [
|
| 168 |
+
{"name": "Alloy Steel AISI 4140 Bright Bars - All 5.8 meter longs"},
|
| 169 |
+
{"name": "Aluminium Sheets & Plates"},
|
| 170 |
+
{"name": "Test product"}
|
| 171 |
+
]
|
| 172 |
+
}
|
| 173 |
+
]
|
| 174 |
+
}
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
If you want to fetch all fields except few you can use exclude(-) operator. For example in the case above if we want to fetch all fields except `name` field, here is how we could do it
|
| 178 |
+
`GET /api/product.template/?query={-name}`
|
| 179 |
+
|
| 180 |
+
```js
|
| 181 |
+
{
|
| 182 |
+
"count": 3,
|
| 183 |
+
"prev": null,
|
| 184 |
+
"current": 1,
|
| 185 |
+
"next": null,
|
| 186 |
+
"total_pages": 1,
|
| 187 |
+
"result": [
|
| 188 |
+
{
|
| 189 |
+
"id": 1,
|
| 190 |
+
... // All fields except name
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"id": 2
|
| 194 |
+
... // All fields except name
|
| 195 |
+
}
|
| 196 |
+
...
|
| 197 |
+
]
|
| 198 |
+
}
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
There is also a wildcard(\*) operator which can be used to fetch all fields, Below is an example which shows how you can fetch all product's fields but under `related_products` field get all fields except `id`.
|
| 202 |
+
|
| 203 |
+
`GET /api/product.template/?query={*, related_products{-id}}`
|
| 204 |
+
|
| 205 |
+
```js
|
| 206 |
+
{
|
| 207 |
+
"count": 3,
|
| 208 |
+
"prev": null,
|
| 209 |
+
"current": 1,
|
| 210 |
+
"next": null,
|
| 211 |
+
"total_pages": 1,
|
| 212 |
+
"result": [
|
| 213 |
+
{
|
| 214 |
+
"id": 1,
|
| 215 |
+
"name": "Pen",
|
| 216 |
+
"related_products"{
|
| 217 |
+
"name": "Pencil",
|
| 218 |
+
... // All fields except id
|
| 219 |
+
}
|
| 220 |
+
... // All fields
|
| 221 |
+
},
|
| 222 |
+
...
|
| 223 |
+
]
|
| 224 |
+
}
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
**If you don't specify query parameter all fields will be returned.**
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
* **filter (optional):**
|
| 231 |
+
|
| 232 |
+
This is used to filter out data returned. For example if we want to get all products with id ranging from 60 to 70, here's how we would do it.
|
| 233 |
+
|
| 234 |
+
`GET /api/product.template/?query={id, name}&filter=[["id", ">", 60], ["id", "<", 70]]`
|
| 235 |
+
|
| 236 |
+
```js
|
| 237 |
+
{
|
| 238 |
+
"count": 3,
|
| 239 |
+
"prev": null,
|
| 240 |
+
"current": 1,
|
| 241 |
+
"next": null,
|
| 242 |
+
"total_pages": 1,
|
| 243 |
+
"result": [
|
| 244 |
+
{
|
| 245 |
+
"id": 67,
|
| 246 |
+
"name": "Crown Paints Economy Superplus Emulsion"
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"id": 69,
|
| 250 |
+
"name": "Crown Paints Permacote"
|
| 251 |
+
}
|
| 252 |
+
]
|
| 253 |
+
}
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
* **page_size (optional) & page (optional):**
|
| 257 |
+
|
| 258 |
+
These two allows us to do pagination. Hre page_size is used to specify number of records on a single page and page is used to specify the current page. For example if we want our page_size to be 5 records and we want to fetch data on page 3 here is how we would do it.
|
| 259 |
+
|
| 260 |
+
`GET /api/product.template/?query={id, name}&page_size=5&page=3`
|
| 261 |
+
|
| 262 |
+
```js
|
| 263 |
+
{
|
| 264 |
+
"count": 5,
|
| 265 |
+
"prev": 2,
|
| 266 |
+
"current": 3,
|
| 267 |
+
"next": 4,
|
| 268 |
+
"total_pages": 15,
|
| 269 |
+
"result": [
|
| 270 |
+
{"id": 141, "name": "Borewell Slotting Pipes"},
|
| 271 |
+
{"id": 114, "name": "Bright Bars"},
|
| 272 |
+
{"id": 128, "name": "Chain Link Fence"},
|
| 273 |
+
{"id": 111, "name": "Cold Rolled Sheets - CRCA & GI Sheets"},
|
| 274 |
+
{"id": 62, "name": "Crown Paints Acrylic Primer/Sealer Undercoat"}
|
| 275 |
+
]
|
| 276 |
+
}
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
Note: `prev`, `current`, `next` and `total_pages` shows the previous page, current page, next page and the total number of pages respectively.
|
| 280 |
+
|
| 281 |
+
* **limit (optional):**
|
| 282 |
+
|
| 283 |
+
This is used to limit the number of results returned on a request regardless of pagination. For example
|
| 284 |
+
|
| 285 |
+
`GET /api/product.template/?query={id, name}&limit=3`
|
| 286 |
+
|
| 287 |
+
```js
|
| 288 |
+
{
|
| 289 |
+
"count": 3,
|
| 290 |
+
"prev": null,
|
| 291 |
+
"current": 1,
|
| 292 |
+
"next": null,
|
| 293 |
+
"total_pages": 1,
|
| 294 |
+
"result": [
|
| 295 |
+
{"id": 16, "name": "Alaf Resincot Steel Roof-16"},
|
| 296 |
+
{"id": 18, "name": "Alaf Resincot Steel Roof-43"},
|
| 297 |
+
{"id": 95, "name": "Alaf versatile steel roof"}
|
| 298 |
+
]
|
| 299 |
+
}
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
### Model record:
|
| 303 |
+
|
| 304 |
+
`GET /api/{model}/{id}`
|
| 305 |
+
#### Parameters
|
| 306 |
+
* **query (optional):**
|
| 307 |
+
|
| 308 |
+
Here query parameter works exactly the same as explained before except it selects fields on a single record. For example
|
| 309 |
+
|
| 310 |
+
`GET /api/product.template/95/?query={id, name}`
|
| 311 |
+
|
| 312 |
+
```js
|
| 313 |
+
{
|
| 314 |
+
"id": 95,
|
| 315 |
+
"name": "Alaf versatile steel roof"
|
| 316 |
+
}
|
| 317 |
+
```
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
## 2. POST
|
| 321 |
+
|
| 322 |
+
`POST /api/{model}/`
|
| 323 |
+
#### Headers
|
| 324 |
+
* Content-Type: application/json
|
| 325 |
+
#### Parameters
|
| 326 |
+
* **data (mandatory):**
|
| 327 |
+
|
| 328 |
+
This is used to pass data to be posted. For example
|
| 329 |
+
|
| 330 |
+
`POST /api/product.public.category/`
|
| 331 |
+
|
| 332 |
+
Request Body
|
| 333 |
+
|
| 334 |
+
```js
|
| 335 |
+
{
|
| 336 |
+
"params": {
|
| 337 |
+
"data": {
|
| 338 |
+
"name": "Test category_2"
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
Response
|
| 345 |
+
|
| 346 |
+
```js
|
| 347 |
+
{
|
| 348 |
+
"jsonrpc": "2.0",
|
| 349 |
+
"id": null,
|
| 350 |
+
"result": 398
|
| 351 |
+
}
|
| 352 |
+
```
|
| 353 |
+
|
| 354 |
+
The number on `result` is the `id` of the newly created record.
|
| 355 |
+
|
| 356 |
+
* **context (optional):**
|
| 357 |
+
|
| 358 |
+
This is used to pass any context if it's needed when creating new record. The format of passing it is
|
| 359 |
+
|
| 360 |
+
Request Body
|
| 361 |
+
|
| 362 |
+
```js
|
| 363 |
+
{
|
| 364 |
+
"params": {
|
| 365 |
+
"context": {
|
| 366 |
+
"context_1": "context_1_value",
|
| 367 |
+
"context_2": "context_2_value",
|
| 368 |
+
....
|
| 369 |
+
},
|
| 370 |
+
"data": {
|
| 371 |
+
"field_1": "field_1_value",
|
| 372 |
+
"field_2": "field_2_value",
|
| 373 |
+
....
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
## 3. PUT
|
| 380 |
+
|
| 381 |
+
### Model records:
|
| 382 |
+
|
| 383 |
+
`PUT /api/{model}/`
|
| 384 |
+
#### Headers
|
| 385 |
+
* Content-Type: application/json
|
| 386 |
+
#### Parameters
|
| 387 |
+
* **data (mandatory):**
|
| 388 |
+
|
| 389 |
+
This is used to pass data to update, it works with filter parameter, See example below
|
| 390 |
+
|
| 391 |
+
* **filter (mandatory):**
|
| 392 |
+
|
| 393 |
+
This is used to filter data to update. For example
|
| 394 |
+
|
| 395 |
+
`PUT /api/product.template/`
|
| 396 |
+
|
| 397 |
+
Request Body
|
| 398 |
+
|
| 399 |
+
```js
|
| 400 |
+
{
|
| 401 |
+
"params": {
|
| 402 |
+
"filter": [["id", "=", 95]],
|
| 403 |
+
"data": {
|
| 404 |
+
"name": "Test product"
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
```
|
| 409 |
+
|
| 410 |
+
Response
|
| 411 |
+
|
| 412 |
+
```js
|
| 413 |
+
{
|
| 414 |
+
"jsonrpc": "2.0",
|
| 415 |
+
"id": null,
|
| 416 |
+
"result": true
|
| 417 |
+
}
|
| 418 |
+
```
|
| 419 |
+
|
| 420 |
+
Note: If the result is true it means success and if false or otherwise it means there was an error during update.
|
| 421 |
+
|
| 422 |
+
* **context (optional):**
|
| 423 |
+
Just like in GET context is used to pass any context associated with record update. The format of passing it is
|
| 424 |
+
|
| 425 |
+
Request Body
|
| 426 |
+
|
| 427 |
+
```js
|
| 428 |
+
{
|
| 429 |
+
"params": {
|
| 430 |
+
"context": {
|
| 431 |
+
"context_1": "context_1_value",
|
| 432 |
+
"context_2": "context_2_value",
|
| 433 |
+
....
|
| 434 |
+
},
|
| 435 |
+
"filter": [["id", "=", 95]],
|
| 436 |
+
"data": {
|
| 437 |
+
"field_1": "field_1_value",
|
| 438 |
+
"field_2": "field_2_value",
|
| 439 |
+
....
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
```
|
| 444 |
+
|
| 445 |
+
* **operation (optional)**:
|
| 446 |
+
|
| 447 |
+
This is only applied to `one2many` and `many2many` fields. The concept is sometimes you might not want to replace all records on either `one2many` or `many2many` fields, instead you might want to add other records or remove some records, this is where put operations comes in place. Thre are basically three PUT operations which are push, pop and delete.
|
| 448 |
+
* push is used to add/append other records to existing linked records
|
| 449 |
+
* pop is used to remove/unlink some records from the record being updated but it doesn't delete them on the system
|
| 450 |
+
* delete is used to remove/unlink and delete records permanently on the system
|
| 451 |
+
|
| 452 |
+
For example here is how you would update `related_product_ids` which is `many2many` field with PUT operations
|
| 453 |
+
|
| 454 |
+
`PUT /api/product.template/`
|
| 455 |
+
|
| 456 |
+
Request Body
|
| 457 |
+
|
| 458 |
+
```js
|
| 459 |
+
{
|
| 460 |
+
"params": {
|
| 461 |
+
"filter": [["id", "=", 95]],
|
| 462 |
+
"data": {
|
| 463 |
+
"related_product_ids": {
|
| 464 |
+
"push": [102, 30],
|
| 465 |
+
"pop": [45],
|
| 466 |
+
"delete": [55]
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
}
|
| 470 |
+
}
|
| 471 |
+
```
|
| 472 |
+
|
| 473 |
+
This will append product with ids 102 and 30 as related products to product with id 95 and from there unlink product with id 45 and again unlink product with id 55 and delete it from the system. So if befor this request product with id 95 had [20, 37, 45, 55] as related product ids, after this request it will be [20, 37, 102, 30].
|
| 474 |
+
|
| 475 |
+
Note: You can use one operation or two or all three at a time depending on what you want to update on your field. If you dont use these operations on `one2many` and `many2many` fields, existing values will be replaced by new values passed, so you need to be very carefull on this part.
|
| 476 |
+
|
| 477 |
+
Response:
|
| 478 |
+
|
| 479 |
+
```js
|
| 480 |
+
{
|
| 481 |
+
"jsonrpc": "2.0",
|
| 482 |
+
"id": null,
|
| 483 |
+
"result": true
|
| 484 |
+
}
|
| 485 |
+
```
|
| 486 |
+
|
| 487 |
+
### Model record:
|
| 488 |
+
|
| 489 |
+
`PUT /api/{model}/{id}`
|
| 490 |
+
#### Headers
|
| 491 |
+
* Content-Type: application/json
|
| 492 |
+
#### Parameters
|
| 493 |
+
* data (mandatory)
|
| 494 |
+
* context (optional)
|
| 495 |
+
* PUT operation(push, pop, delete) (optional)
|
| 496 |
+
|
| 497 |
+
All parameters works the same as explained on previous section, what changes is that here they apply to a single record being updated and we don't have filter parameter because `id` of record to be updated is passed on URL as `{id}`. Example to give us an idea of how this works.
|
| 498 |
+
|
| 499 |
+
`PUT /api/product.template/95/`
|
| 500 |
+
|
| 501 |
+
Request Body
|
| 502 |
+
|
| 503 |
+
```js
|
| 504 |
+
{
|
| 505 |
+
"params": {
|
| 506 |
+
"data": {
|
| 507 |
+
"related_product_ids": {
|
| 508 |
+
"push": [102, 30],
|
| 509 |
+
"pop": [45],
|
| 510 |
+
"delete": [55]
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
}
|
| 514 |
+
}
|
| 515 |
+
```
|
| 516 |
+
|
| 517 |
+
## 4. DELETE
|
| 518 |
+
|
| 519 |
+
### Model records:
|
| 520 |
+
|
| 521 |
+
`DELETE /api/{model}/`
|
| 522 |
+
#### Parameters
|
| 523 |
+
* **filter (mandatory):**
|
| 524 |
+
|
| 525 |
+
This is used to filter data to delete. For example
|
| 526 |
+
|
| 527 |
+
`DELETE /api/product.template/?filter=[["id", "=", 95]]`
|
| 528 |
+
|
| 529 |
+
Response
|
| 530 |
+
|
| 531 |
+
```js
|
| 532 |
+
{
|
| 533 |
+
"result": true
|
| 534 |
+
}
|
| 535 |
+
```
|
| 536 |
+
|
| 537 |
+
Note: If the result is true it means success and if false or otherwise it means there was an error during deletion.
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
### Model records:
|
| 541 |
+
|
| 542 |
+
`DELETE /api/{model}/{id}`
|
| 543 |
+
#### Parameters
|
| 544 |
+
This takes no parameter and we don't have filter parameter because `id` of record to be deleted is passed on URL as `{id}`. Example to give us an idea of how this works.
|
| 545 |
+
|
| 546 |
+
`DELETE /api/product.template/95/`
|
| 547 |
+
|
| 548 |
+
Response
|
| 549 |
+
|
| 550 |
+
```js
|
| 551 |
+
{
|
| 552 |
+
"result": true
|
| 553 |
+
}
|
| 554 |
+
```
|
| 555 |
+
|
| 556 |
+
## Calling Model's Function
|
| 557 |
+
|
| 558 |
+
Sometimes you might need to call model's function or a function bound to a record, inorder to do so, send a `POST` request with a body containing arguments(args) and keyword arguments(kwargs) required by the function you want to call.
|
| 559 |
+
|
| 560 |
+
Below is how you can call model's function
|
| 561 |
+
|
| 562 |
+
`POST /object/{model}/{function name}`
|
| 563 |
+
|
| 564 |
+
Request Body
|
| 565 |
+
|
| 566 |
+
```js
|
| 567 |
+
{
|
| 568 |
+
"params": {
|
| 569 |
+
"args": [arg1, arg2, ..],
|
| 570 |
+
"kwargs ": {
|
| 571 |
+
"key1": "value1",
|
| 572 |
+
"key2": "value2",
|
| 573 |
+
...
|
| 574 |
+
}
|
| 575 |
+
}
|
| 576 |
+
}
|
| 577 |
+
```
|
| 578 |
+
|
| 579 |
+
And below is how you can call a function bound to a record
|
| 580 |
+
|
| 581 |
+
`POST /object/{model}/{record_id}/{function name}`
|
| 582 |
+
|
| 583 |
+
Request Body
|
| 584 |
+
|
| 585 |
+
```js
|
| 586 |
+
{
|
| 587 |
+
"params": {
|
| 588 |
+
"args": [arg1, arg2, ..],
|
| 589 |
+
"kwargs ": {
|
| 590 |
+
"key1": "value1",
|
| 591 |
+
"key2": "value2",
|
| 592 |
+
...
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
}
|
| 596 |
+
```
|
| 597 |
+
|
| 598 |
+
In both cases the response will be the result returned by the function called
|
rest-api/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
from . import controllers
|
| 4 |
+
from . import models
|
rest-api/__manifest__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
{
|
| 3 |
+
'name': "Odoo REST API",
|
| 4 |
+
|
| 5 |
+
'summary': """
|
| 6 |
+
Odoo REST API""",
|
| 7 |
+
|
| 8 |
+
'description': """
|
| 9 |
+
Odoo REST API
|
| 10 |
+
""",
|
| 11 |
+
|
| 12 |
+
'author': "Yezileli Ilomo",
|
| 13 |
+
'website': "https://github.com/yezyilomo/odoo-rest-api",
|
| 14 |
+
|
| 15 |
+
# Categories can be used to filter modules in modules listing
|
| 16 |
+
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
|
| 17 |
+
# for the full list
|
| 18 |
+
'category': 'developers',
|
| 19 |
+
'version': '0.1',
|
| 20 |
+
|
| 21 |
+
# any module necessary for this one to work correctly
|
| 22 |
+
'depends': ['base'],
|
| 23 |
+
|
| 24 |
+
# always loaded
|
| 25 |
+
'data': [
|
| 26 |
+
# 'security/ir.model.access.csv',
|
| 27 |
+
'views/views.xml',
|
| 28 |
+
'views/templates.xml',
|
| 29 |
+
],
|
| 30 |
+
# only loaded in demonstration mode
|
| 31 |
+
'demo': [
|
| 32 |
+
'demo/demo.xml',
|
| 33 |
+
],
|
| 34 |
+
|
| 35 |
+
"application": True,
|
| 36 |
+
"installable": True,
|
| 37 |
+
"auto_install": False,
|
| 38 |
+
|
| 39 |
+
'external_dependencies': {
|
| 40 |
+
'python': ['pypeg2']
|
| 41 |
+
}
|
| 42 |
+
}
|
rest-api/controllers/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
from . import controllers
|
rest-api/controllers/controllers.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import json
|
| 3 |
+
import math
|
| 4 |
+
import logging
|
| 5 |
+
import requests
|
| 6 |
+
|
| 7 |
+
from odoo import http, _, exceptions
|
| 8 |
+
from odoo.http import request
|
| 9 |
+
|
| 10 |
+
from .serializers import Serializer
|
| 11 |
+
from .exceptions import QueryFormatError
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
_logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def error_response(error, msg):
|
| 18 |
+
return {
|
| 19 |
+
"jsonrpc": "2.0",
|
| 20 |
+
"id": None,
|
| 21 |
+
"error": {
|
| 22 |
+
"code": 200,
|
| 23 |
+
"message": msg,
|
| 24 |
+
"data": {
|
| 25 |
+
"name": str(error),
|
| 26 |
+
"debug": "",
|
| 27 |
+
"message": msg,
|
| 28 |
+
"arguments": list(error.args),
|
| 29 |
+
"exception_type": type(error).__name__
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class OdooAPI(http.Controller):
|
| 36 |
+
@http.route(
|
| 37 |
+
'/auth/',
|
| 38 |
+
type='json', auth='none', methods=["POST"], csrf=False)
|
| 39 |
+
def authenticate(self, *args, **post):
|
| 40 |
+
try:
|
| 41 |
+
login = post["login"]
|
| 42 |
+
except KeyError:
|
| 43 |
+
raise exceptions.AccessDenied(message='`login` is required.')
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
password = post["password"]
|
| 47 |
+
except KeyError:
|
| 48 |
+
raise exceptions.AccessDenied(message='`password` is required.')
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
db = post["db"]
|
| 52 |
+
except KeyError:
|
| 53 |
+
raise exceptions.AccessDenied(message='`db` is required.')
|
| 54 |
+
|
| 55 |
+
http.request.session.authenticate(db, login, password)
|
| 56 |
+
res = request.env['ir.http'].session_info()
|
| 57 |
+
return res
|
| 58 |
+
|
| 59 |
+
@http.route(
|
| 60 |
+
'/object/<string:model>/<string:function>',
|
| 61 |
+
type='json', auth='user', methods=["POST"], csrf=False)
|
| 62 |
+
def call_model_function(self, model, function, **post):
|
| 63 |
+
args = []
|
| 64 |
+
kwargs = {}
|
| 65 |
+
if "args" in post:
|
| 66 |
+
args = post["args"]
|
| 67 |
+
if "kwargs" in post:
|
| 68 |
+
kwargs = post["kwargs"]
|
| 69 |
+
model = request.env[model]
|
| 70 |
+
result = getattr(model, function)(*args, **kwargs)
|
| 71 |
+
return result
|
| 72 |
+
|
| 73 |
+
@http.route(
|
| 74 |
+
'/object/<string:model>/<int:rec_id>/<string:function>',
|
| 75 |
+
type='json', auth='user', methods=["POST"], csrf=False)
|
| 76 |
+
def call_obj_function(self, model, rec_id, function, **post):
|
| 77 |
+
args = []
|
| 78 |
+
kwargs = {}
|
| 79 |
+
if "args" in post:
|
| 80 |
+
args = post["args"]
|
| 81 |
+
if "kwargs" in post:
|
| 82 |
+
kwargs = post["kwargs"]
|
| 83 |
+
obj = request.env[model].browse(rec_id).ensure_one()
|
| 84 |
+
result = getattr(obj, function)(*args, **kwargs)
|
| 85 |
+
return result
|
| 86 |
+
|
| 87 |
+
@http.route(
|
| 88 |
+
'/api/<string:model>',
|
| 89 |
+
type='http', auth='user', methods=['GET'], csrf=False)
|
| 90 |
+
def get_model_data(self, model, **params):
|
| 91 |
+
try:
|
| 92 |
+
records = request.env[model].search([])
|
| 93 |
+
except KeyError as e:
|
| 94 |
+
msg = "The model `%s` does not exist." % model
|
| 95 |
+
res = error_response(e, msg)
|
| 96 |
+
return http.Response(
|
| 97 |
+
json.dumps(res),
|
| 98 |
+
status=200,
|
| 99 |
+
mimetype='application/json'
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if "query" in params:
|
| 103 |
+
query = params["query"]
|
| 104 |
+
else:
|
| 105 |
+
query = "{*}"
|
| 106 |
+
|
| 107 |
+
if "order" in params:
|
| 108 |
+
orders = json.loads(params["order"])
|
| 109 |
+
else:
|
| 110 |
+
orders = ""
|
| 111 |
+
|
| 112 |
+
if "filter" in params:
|
| 113 |
+
filters = json.loads(params["filter"])
|
| 114 |
+
records = request.env[model].search(filters, order=orders)
|
| 115 |
+
|
| 116 |
+
prev_page = None
|
| 117 |
+
next_page = None
|
| 118 |
+
total_page_number = 1
|
| 119 |
+
current_page = 1
|
| 120 |
+
|
| 121 |
+
if "page_size" in params:
|
| 122 |
+
page_size = int(params["page_size"])
|
| 123 |
+
count = len(records)
|
| 124 |
+
total_page_number = math.ceil(count/page_size)
|
| 125 |
+
|
| 126 |
+
if "page" in params:
|
| 127 |
+
current_page = int(params["page"])
|
| 128 |
+
else:
|
| 129 |
+
current_page = 1 # Default page Number
|
| 130 |
+
start = page_size*(current_page-1)
|
| 131 |
+
stop = current_page*page_size
|
| 132 |
+
records = records[start:stop]
|
| 133 |
+
next_page = current_page+1 \
|
| 134 |
+
if 0 < current_page + 1 <= total_page_number \
|
| 135 |
+
else None
|
| 136 |
+
prev_page = current_page-1 \
|
| 137 |
+
if 0 < current_page - 1 <= total_page_number \
|
| 138 |
+
else None
|
| 139 |
+
|
| 140 |
+
if "limit" in params:
|
| 141 |
+
limit = int(params["limit"])
|
| 142 |
+
records = records[0:limit]
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
serializer = Serializer(records, query, many=True)
|
| 146 |
+
data = serializer.data
|
| 147 |
+
except (SyntaxError, QueryFormatError) as e:
|
| 148 |
+
res = error_response(e, e.msg)
|
| 149 |
+
return http.Response(
|
| 150 |
+
json.dumps(res),
|
| 151 |
+
status=200,
|
| 152 |
+
mimetype='application/json'
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
res = {
|
| 156 |
+
"count": len(records),
|
| 157 |
+
"prev": prev_page,
|
| 158 |
+
"current": current_page,
|
| 159 |
+
"next": next_page,
|
| 160 |
+
"total_pages": total_page_number,
|
| 161 |
+
"result": data
|
| 162 |
+
}
|
| 163 |
+
return http.Response(
|
| 164 |
+
json.dumps(res),
|
| 165 |
+
status=200,
|
| 166 |
+
mimetype='application/json'
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
@http.route(
|
| 170 |
+
'/api/<string:model>/<int:rec_id>',
|
| 171 |
+
type='http', auth='user', methods=['GET'], csrf=False)
|
| 172 |
+
def get_model_rec(self, model, rec_id, **params):
|
| 173 |
+
try:
|
| 174 |
+
records = request.env[model].search([])
|
| 175 |
+
except KeyError as e:
|
| 176 |
+
msg = "The model `%s` does not exist." % model
|
| 177 |
+
res = error_response(e, msg)
|
| 178 |
+
return http.Response(
|
| 179 |
+
json.dumps(res),
|
| 180 |
+
status=200,
|
| 181 |
+
mimetype='application/json'
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
if "query" in params:
|
| 185 |
+
query = params["query"]
|
| 186 |
+
else:
|
| 187 |
+
query = "{*}"
|
| 188 |
+
|
| 189 |
+
# TODO: Handle the error raised by `ensure_one`
|
| 190 |
+
record = records.browse(rec_id).ensure_one()
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
serializer = Serializer(record, query)
|
| 194 |
+
data = serializer.data
|
| 195 |
+
except (SyntaxError, QueryFormatError) as e:
|
| 196 |
+
res = error_response(e, e.msg)
|
| 197 |
+
return http.Response(
|
| 198 |
+
json.dumps(res),
|
| 199 |
+
status=200,
|
| 200 |
+
mimetype='application/json'
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
return http.Response(
|
| 204 |
+
json.dumps(data),
|
| 205 |
+
status=200,
|
| 206 |
+
mimetype='application/json'
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
@http.route(
|
| 210 |
+
'/api/<string:model>/',
|
| 211 |
+
type='json', auth="user", methods=['POST'], csrf=False)
|
| 212 |
+
def post_model_data(self, model, **post):
|
| 213 |
+
try:
|
| 214 |
+
data = post['data']
|
| 215 |
+
except KeyError:
|
| 216 |
+
msg = "`data` parameter is not found on POST request body"
|
| 217 |
+
raise exceptions.ValidationError(msg)
|
| 218 |
+
|
| 219 |
+
try:
|
| 220 |
+
model_to_post = request.env[model]
|
| 221 |
+
except KeyError:
|
| 222 |
+
msg = "The model `%s` does not exist." % model
|
| 223 |
+
raise exceptions.ValidationError(msg)
|
| 224 |
+
|
| 225 |
+
# TODO: Handle data validation
|
| 226 |
+
|
| 227 |
+
if "context" in post:
|
| 228 |
+
context = post["context"]
|
| 229 |
+
record = model_to_post.with_context(**context).create(data)
|
| 230 |
+
else:
|
| 231 |
+
record = model_to_post.create(data)
|
| 232 |
+
return record.id
|
| 233 |
+
|
| 234 |
+
# This is for single record update
|
| 235 |
+
@http.route(
|
| 236 |
+
'/api/<string:model>/<int:rec_id>/',
|
| 237 |
+
type='json', auth="user", methods=['PUT'], csrf=False)
|
| 238 |
+
def put_model_record(self, model, rec_id, **post):
|
| 239 |
+
try:
|
| 240 |
+
data = post['data']
|
| 241 |
+
except KeyError:
|
| 242 |
+
msg = "`data` parameter is not found on PUT request body"
|
| 243 |
+
raise exceptions.ValidationError(msg)
|
| 244 |
+
|
| 245 |
+
try:
|
| 246 |
+
model_to_put = request.env[model]
|
| 247 |
+
except KeyError:
|
| 248 |
+
msg = "The model `%s` does not exist." % model
|
| 249 |
+
raise exceptions.ValidationError(msg)
|
| 250 |
+
|
| 251 |
+
if "context" in post:
|
| 252 |
+
# TODO: Handle error raised by `ensure_one`
|
| 253 |
+
rec = model_to_put.with_context(**post["context"])\
|
| 254 |
+
.browse(rec_id).ensure_one()
|
| 255 |
+
else:
|
| 256 |
+
rec = model_to_put.browse(rec_id).ensure_one()
|
| 257 |
+
|
| 258 |
+
# TODO: Handle data validation
|
| 259 |
+
for field in data:
|
| 260 |
+
if isinstance(data[field], dict):
|
| 261 |
+
operations = []
|
| 262 |
+
for operation in data[field]:
|
| 263 |
+
if operation == "push":
|
| 264 |
+
operations.extend(
|
| 265 |
+
(4, rec_id, _)
|
| 266 |
+
for rec_id
|
| 267 |
+
in data[field].get("push")
|
| 268 |
+
)
|
| 269 |
+
elif operation == "pop":
|
| 270 |
+
operations.extend(
|
| 271 |
+
(3, rec_id, _)
|
| 272 |
+
for rec_id
|
| 273 |
+
in data[field].get("pop")
|
| 274 |
+
)
|
| 275 |
+
elif operation == "delete":
|
| 276 |
+
operations.extend(
|
| 277 |
+
(2, rec_id, _)
|
| 278 |
+
for rec_id
|
| 279 |
+
in data[field].get("delete")
|
| 280 |
+
)
|
| 281 |
+
else:
|
| 282 |
+
data[field].pop(operation) # Invalid operation
|
| 283 |
+
|
| 284 |
+
data[field] = operations
|
| 285 |
+
elif isinstance(data[field], list):
|
| 286 |
+
data[field] = [(6, _, data[field])] # Replace operation
|
| 287 |
+
else:
|
| 288 |
+
pass
|
| 289 |
+
|
| 290 |
+
try:
|
| 291 |
+
return rec.write(data)
|
| 292 |
+
except Exception as e:
|
| 293 |
+
# TODO: Return error message(e.msg) on a response
|
| 294 |
+
return False
|
| 295 |
+
|
| 296 |
+
# This is for bulk update
|
| 297 |
+
@http.route(
|
| 298 |
+
'/api/<string:model>/',
|
| 299 |
+
type='json', auth="user", methods=['PUT'], csrf=False)
|
| 300 |
+
def put_model_records(self, model, **post):
|
| 301 |
+
try:
|
| 302 |
+
data = post['data']
|
| 303 |
+
except KeyError:
|
| 304 |
+
msg = "`data` parameter is not found on PUT request body"
|
| 305 |
+
raise exceptions.ValidationError(msg)
|
| 306 |
+
|
| 307 |
+
try:
|
| 308 |
+
model_to_put = request.env[model]
|
| 309 |
+
except KeyError:
|
| 310 |
+
msg = "The model `%s` does not exist." % model
|
| 311 |
+
raise exceptions.ValidationError(msg)
|
| 312 |
+
|
| 313 |
+
# TODO: Handle errors on filter
|
| 314 |
+
filters = post["filter"]
|
| 315 |
+
|
| 316 |
+
if "context" in post:
|
| 317 |
+
recs = model_to_put.with_context(**post["context"])\
|
| 318 |
+
.search(filters)
|
| 319 |
+
else:
|
| 320 |
+
recs = model_to_put.search(filters)
|
| 321 |
+
|
| 322 |
+
# TODO: Handle data validation
|
| 323 |
+
for field in data:
|
| 324 |
+
if isinstance(data[field], dict):
|
| 325 |
+
operations = []
|
| 326 |
+
for operation in data[field]:
|
| 327 |
+
if operation == "push":
|
| 328 |
+
operations.extend(
|
| 329 |
+
(4, rec_id, _)
|
| 330 |
+
for rec_id
|
| 331 |
+
in data[field].get("push")
|
| 332 |
+
)
|
| 333 |
+
elif operation == "pop":
|
| 334 |
+
operations.extend(
|
| 335 |
+
(3, rec_id, _)
|
| 336 |
+
for rec_id
|
| 337 |
+
in data[field].get("pop")
|
| 338 |
+
)
|
| 339 |
+
elif operation == "delete":
|
| 340 |
+
operations.extend(
|
| 341 |
+
(2, rec_id, _)
|
| 342 |
+
for rec_id in
|
| 343 |
+
data[field].get("delete")
|
| 344 |
+
)
|
| 345 |
+
else:
|
| 346 |
+
pass # Invalid operation
|
| 347 |
+
|
| 348 |
+
data[field] = operations
|
| 349 |
+
elif isinstance(data[field], list):
|
| 350 |
+
data[field] = [(6, _, data[field])] # Replace operation
|
| 351 |
+
else:
|
| 352 |
+
pass
|
| 353 |
+
|
| 354 |
+
if recs.exists():
|
| 355 |
+
try:
|
| 356 |
+
return recs.write(data)
|
| 357 |
+
except Exception as e:
|
| 358 |
+
# TODO: Return error message(e.msg) on a response
|
| 359 |
+
return False
|
| 360 |
+
else:
|
| 361 |
+
# No records to update
|
| 362 |
+
return True
|
| 363 |
+
|
| 364 |
+
# This is for deleting one record
|
| 365 |
+
@http.route(
|
| 366 |
+
'/api/<string:model>/<int:rec_id>/',
|
| 367 |
+
type='http', auth="user", methods=['DELETE'], csrf=False)
|
| 368 |
+
def delete_model_record(self, model, rec_id, **post):
|
| 369 |
+
try:
|
| 370 |
+
model_to_del_rec = request.env[model]
|
| 371 |
+
except KeyError as e:
|
| 372 |
+
msg = "The model `%s` does not exist." % model
|
| 373 |
+
res = error_response(e, msg)
|
| 374 |
+
return http.Response(
|
| 375 |
+
json.dumps(res),
|
| 376 |
+
status=200,
|
| 377 |
+
mimetype='application/json'
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
# TODO: Handle error raised by `ensure_one`
|
| 381 |
+
rec = model_to_del_rec.browse(rec_id).ensure_one()
|
| 382 |
+
|
| 383 |
+
try:
|
| 384 |
+
is_deleted = rec.unlink()
|
| 385 |
+
res = {
|
| 386 |
+
"result": is_deleted
|
| 387 |
+
}
|
| 388 |
+
return http.Response(
|
| 389 |
+
json.dumps(res),
|
| 390 |
+
status=200,
|
| 391 |
+
mimetype='application/json'
|
| 392 |
+
)
|
| 393 |
+
except Exception as e:
|
| 394 |
+
res = error_response(e, str(e))
|
| 395 |
+
return http.Response(
|
| 396 |
+
json.dumps(res),
|
| 397 |
+
status=200,
|
| 398 |
+
mimetype='application/json'
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
# This is for bulk deletion
|
| 402 |
+
@http.route(
|
| 403 |
+
'/api/<string:model>/',
|
| 404 |
+
type='http', auth="user", methods=['DELETE'], csrf=False)
|
| 405 |
+
def delete_model_records(self, model, **post):
|
| 406 |
+
filters = json.loads(post["filter"])
|
| 407 |
+
|
| 408 |
+
try:
|
| 409 |
+
model_to_del_rec = request.env[model]
|
| 410 |
+
except KeyError as e:
|
| 411 |
+
msg = "The model `%s` does not exist." % model
|
| 412 |
+
res = error_response(e, msg)
|
| 413 |
+
return http.Response(
|
| 414 |
+
json.dumps(res),
|
| 415 |
+
status=200,
|
| 416 |
+
mimetype='application/json'
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
# TODO: Handle error raised by `filters`
|
| 420 |
+
recs = model_to_del_rec.search(filters)
|
| 421 |
+
|
| 422 |
+
try:
|
| 423 |
+
is_deleted = recs.unlink()
|
| 424 |
+
res = {
|
| 425 |
+
"result": is_deleted
|
| 426 |
+
}
|
| 427 |
+
return http.Response(
|
| 428 |
+
json.dumps(res),
|
| 429 |
+
status=200,
|
| 430 |
+
mimetype='application/json'
|
| 431 |
+
)
|
| 432 |
+
except Exception as e:
|
| 433 |
+
res = error_response(e, str(e))
|
| 434 |
+
return http.Response(
|
| 435 |
+
json.dumps(res),
|
| 436 |
+
status=200,
|
| 437 |
+
mimetype='application/json'
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
@http.route(
|
| 441 |
+
'/api/<string:model>/<int:rec_id>/<string:field>',
|
| 442 |
+
type='http', auth="user", methods=['GET'], csrf=False)
|
| 443 |
+
def get_binary_record(self, model, rec_id, field, **post):
|
| 444 |
+
try:
|
| 445 |
+
request.env[model]
|
| 446 |
+
except KeyError as e:
|
| 447 |
+
msg = "The model `%s` does not exist." % model
|
| 448 |
+
res = error_response(e, msg)
|
| 449 |
+
return http.Response(
|
| 450 |
+
json.dumps(res),
|
| 451 |
+
status=200,
|
| 452 |
+
mimetype='application/json'
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
rec = request.env[model].browse(rec_id).ensure_one()
|
| 456 |
+
if rec.exists():
|
| 457 |
+
src = getattr(rec, field).decode("utf-8")
|
| 458 |
+
else:
|
| 459 |
+
src = False
|
| 460 |
+
return http.Response(
|
| 461 |
+
src
|
| 462 |
+
)
|
rest-api/controllers/exceptions.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class QueryFormatError(Exception):
|
| 2 |
+
"""Invalid Query Format."""
|
rest-api/controllers/parser.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
from pypeg2 import List, contiguous, csl, name, optional, parse
|
| 4 |
+
|
| 5 |
+
from .exceptions import QueryFormatError
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class IncludedField(List):
|
| 9 |
+
grammar = name()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ExcludedField(List):
|
| 13 |
+
grammar = contiguous('-', name())
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AllFields(str):
|
| 17 |
+
grammar = '*'
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class BaseArgument(List):
|
| 21 |
+
@property
|
| 22 |
+
def value(self):
|
| 23 |
+
return self[0]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class ArgumentWithoutQuotes(BaseArgument):
|
| 27 |
+
grammar = name(), ':', re.compile(r'[^,:"\'\)]+')
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ArgumentWithSingleQuotes(BaseArgument):
|
| 31 |
+
grammar = name(), ':', "'", re.compile(r'[^\']+'), "'"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class ArgumentWithDoubleQuotes(BaseArgument):
|
| 35 |
+
grammar = name(), ':', '"', re.compile(r'[^"]+'), '"'
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Arguments(List):
|
| 39 |
+
grammar = optional(csl(
|
| 40 |
+
[
|
| 41 |
+
ArgumentWithoutQuotes,
|
| 42 |
+
ArgumentWithSingleQuotes,
|
| 43 |
+
ArgumentWithDoubleQuotes
|
| 44 |
+
],
|
| 45 |
+
separator=','
|
| 46 |
+
))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class ArgumentsBlock(List):
|
| 50 |
+
grammar = optional('(', Arguments, ')')
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def arguments(self):
|
| 54 |
+
if self[0] is None:
|
| 55 |
+
return [] # No arguments
|
| 56 |
+
return self[0]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class ParentField(List):
|
| 60 |
+
"""
|
| 61 |
+
According to ParentField grammar:
|
| 62 |
+
self[0] returns IncludedField instance,
|
| 63 |
+
self[1] returns Block instance
|
| 64 |
+
"""
|
| 65 |
+
@property
|
| 66 |
+
def name(self):
|
| 67 |
+
return self[0].name
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def block(self):
|
| 71 |
+
return self[1]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class BlockBody(List):
|
| 75 |
+
grammar = optional(csl(
|
| 76 |
+
[ParentField, IncludedField, ExcludedField, AllFields],
|
| 77 |
+
separator=','
|
| 78 |
+
))
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Block(List):
|
| 82 |
+
grammar = ArgumentsBlock, '{', BlockBody, '}'
|
| 83 |
+
|
| 84 |
+
@property
|
| 85 |
+
def arguments(self):
|
| 86 |
+
return self[0].arguments
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def body(self):
|
| 90 |
+
return self[1]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ParentField grammar,
|
| 94 |
+
# We don't include `ExcludedField` here because
|
| 95 |
+
# exclude operator(-) on a parent field should
|
| 96 |
+
# raise syntax error, e.g {name, -location{city}}
|
| 97 |
+
# `IncludedField` is a parent field and `Block`
|
| 98 |
+
# contains its sub fields
|
| 99 |
+
ParentField.grammar = IncludedField, Block
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class Parser(object):
|
| 103 |
+
def __init__(self, query):
|
| 104 |
+
self._query = query
|
| 105 |
+
|
| 106 |
+
def get_parsed(self):
|
| 107 |
+
parse_tree = parse(self._query, Block)
|
| 108 |
+
return self._transform_block(parse_tree)
|
| 109 |
+
|
| 110 |
+
def _transform_block(self, block):
|
| 111 |
+
fields = {
|
| 112 |
+
"include": [],
|
| 113 |
+
"exclude": [],
|
| 114 |
+
"arguments": {}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
for argument in block.arguments:
|
| 118 |
+
argument = {str(argument.name): argument.value}
|
| 119 |
+
fields['arguments'].update(argument)
|
| 120 |
+
|
| 121 |
+
for field in block.body:
|
| 122 |
+
# A field may be a parent or included field or excluded field
|
| 123 |
+
field = self._transform_field(field)
|
| 124 |
+
|
| 125 |
+
if isinstance(field, dict):
|
| 126 |
+
# A field is a parent
|
| 127 |
+
fields["include"].append(field)
|
| 128 |
+
elif isinstance(field, IncludedField):
|
| 129 |
+
fields["include"].append(str(field.name))
|
| 130 |
+
elif isinstance(field, ExcludedField):
|
| 131 |
+
fields["exclude"].append(str(field.name))
|
| 132 |
+
elif isinstance(field, AllFields):
|
| 133 |
+
# include all fields
|
| 134 |
+
fields["include"].append("*")
|
| 135 |
+
|
| 136 |
+
if fields["exclude"]:
|
| 137 |
+
# fields['include'] should contain only nested fields
|
| 138 |
+
|
| 139 |
+
# We should add `*` operator in fields['include']
|
| 140 |
+
add_include_all_operator = True
|
| 141 |
+
for field in fields["include"]:
|
| 142 |
+
if field == "*":
|
| 143 |
+
# `*` operator is alredy in fields['include']
|
| 144 |
+
add_include_all_operator = False
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
if isinstance(field, str):
|
| 148 |
+
# Including and excluding fields on the same field level
|
| 149 |
+
msg = (
|
| 150 |
+
"Can not include and exclude fields on the same "
|
| 151 |
+
"field level"
|
| 152 |
+
)
|
| 153 |
+
raise QueryFormatError(msg)
|
| 154 |
+
|
| 155 |
+
if add_include_all_operator:
|
| 156 |
+
# Make sure we include * operator
|
| 157 |
+
fields["include"].append("*")
|
| 158 |
+
return fields
|
| 159 |
+
|
| 160 |
+
def _transform_field(self, field):
|
| 161 |
+
# A field may be a parent or included field or excluded field
|
| 162 |
+
if isinstance(field, ParentField):
|
| 163 |
+
return self._transform_parent_field(field)
|
| 164 |
+
elif isinstance(field, (IncludedField, ExcludedField, AllFields)):
|
| 165 |
+
return field
|
| 166 |
+
|
| 167 |
+
def _transform_parent_field(self, parent_field):
|
| 168 |
+
parent_field_name = str(parent_field.name)
|
| 169 |
+
parent_field_value = self._transform_block(parent_field.block)
|
| 170 |
+
return {parent_field_name: parent_field_value}
|
| 171 |
+
|
rest-api/controllers/serializers.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import datetime
|
| 3 |
+
from itertools import chain
|
| 4 |
+
|
| 5 |
+
from .parser import Parser
|
| 6 |
+
from .exceptions import QueryFormatError
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Serializer(object):
|
| 10 |
+
def __init__(self, record, query="{*}", many=False):
|
| 11 |
+
self.many = many
|
| 12 |
+
self._record = record
|
| 13 |
+
self._raw_query = query
|
| 14 |
+
super().__init__()
|
| 15 |
+
|
| 16 |
+
def get_parsed_restql_query(self):
|
| 17 |
+
parser = Parser(self._raw_query)
|
| 18 |
+
try:
|
| 19 |
+
parsed_restql_query = parser.get_parsed()
|
| 20 |
+
return parsed_restql_query
|
| 21 |
+
except SyntaxError as e:
|
| 22 |
+
msg = "QuerySyntaxError: " + e.msg + " on " + e.text
|
| 23 |
+
raise SyntaxError(msg) from None
|
| 24 |
+
except QueryFormatError as e:
|
| 25 |
+
msg = "QueryFormatError: " + str(e)
|
| 26 |
+
raise QueryFormatError(msg) from None
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def data(self):
|
| 30 |
+
parsed_restql_query = self.get_parsed_restql_query()
|
| 31 |
+
if self.many:
|
| 32 |
+
return [
|
| 33 |
+
self.serialize(rec, parsed_restql_query)
|
| 34 |
+
for rec
|
| 35 |
+
in self._record
|
| 36 |
+
]
|
| 37 |
+
return self.serialize(self._record, parsed_restql_query)
|
| 38 |
+
|
| 39 |
+
@classmethod
|
| 40 |
+
def build_flat_field(cls, rec, field_name):
|
| 41 |
+
all_fields = rec.fields_get()
|
| 42 |
+
if field_name not in all_fields:
|
| 43 |
+
msg = "'%s' field is not found" % field_name
|
| 44 |
+
raise LookupError(msg)
|
| 45 |
+
field_type = rec.fields_get(field_name).get(field_name).get('type')
|
| 46 |
+
if field_type in ['one2many', 'many2many']:
|
| 47 |
+
return {
|
| 48 |
+
field_name: [record.id for record in rec[field_name]]
|
| 49 |
+
}
|
| 50 |
+
elif field_type in ['many2one']:
|
| 51 |
+
return {field_name: rec[field_name].id}
|
| 52 |
+
elif field_type == 'datetime' and rec[field_name]:
|
| 53 |
+
return {
|
| 54 |
+
field_name: rec[field_name].strftime("%Y-%m-%d-%H-%M")
|
| 55 |
+
}
|
| 56 |
+
elif field_type == 'date' and rec[field_name]:
|
| 57 |
+
return {
|
| 58 |
+
field_name: rec[field_name].strftime("%Y-%m-%d")
|
| 59 |
+
}
|
| 60 |
+
elif field_type == 'time' and rec[field_name]:
|
| 61 |
+
return {
|
| 62 |
+
field_name: rec[field_name].strftime("%H-%M-%S")
|
| 63 |
+
}
|
| 64 |
+
elif field_type == "binary" and isinstance(rec[field_name], bytes) and rec[field_name]:
|
| 65 |
+
return {field_name: rec[field_name].decode("utf-8")}
|
| 66 |
+
else:
|
| 67 |
+
return {field_name: rec[field_name]}
|
| 68 |
+
|
| 69 |
+
@classmethod
|
| 70 |
+
def build_nested_field(cls, rec, field_name, nested_parsed_query):
|
| 71 |
+
all_fields = rec.fields_get()
|
| 72 |
+
if field_name not in all_fields:
|
| 73 |
+
msg = "'%s' field is not found" % field_name
|
| 74 |
+
raise LookupError(msg)
|
| 75 |
+
field_type = rec.fields_get(field_name).get(field_name).get('type')
|
| 76 |
+
if field_type in ['one2many', 'many2many']:
|
| 77 |
+
return {
|
| 78 |
+
field_name: [
|
| 79 |
+
cls.serialize(record, nested_parsed_query)
|
| 80 |
+
for record
|
| 81 |
+
in rec[field_name]
|
| 82 |
+
]
|
| 83 |
+
}
|
| 84 |
+
elif field_type in ['many2one']:
|
| 85 |
+
return {
|
| 86 |
+
field_name: cls.serialize(rec[field_name], nested_parsed_query)
|
| 87 |
+
}
|
| 88 |
+
else:
|
| 89 |
+
# Not a neste field
|
| 90 |
+
msg = "'%s' is not a nested field" % field_name
|
| 91 |
+
raise ValueError(msg)
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def serialize(cls, rec, parsed_query):
|
| 95 |
+
data = {}
|
| 96 |
+
|
| 97 |
+
# NOTE: self.parsed_restql_query["include"] not being empty
|
| 98 |
+
# is not a guarantee that the exclude operator(-) has not been
|
| 99 |
+
# used because the same self.parsed_restql_query["include"]
|
| 100 |
+
# is used to store nested fields when the exclude operator(-) is used
|
| 101 |
+
if parsed_query["exclude"]:
|
| 102 |
+
# Exclude fields from a query
|
| 103 |
+
all_fields = rec.fields_get()
|
| 104 |
+
for field in parsed_query["include"]:
|
| 105 |
+
if field == "*":
|
| 106 |
+
continue
|
| 107 |
+
for nested_field, nested_parsed_query in field.items():
|
| 108 |
+
built_nested_field = cls.build_nested_field(
|
| 109 |
+
rec,
|
| 110 |
+
nested_field,
|
| 111 |
+
nested_parsed_query
|
| 112 |
+
)
|
| 113 |
+
data.update(built_nested_field)
|
| 114 |
+
|
| 115 |
+
flat_fields= set(all_fields).symmetric_difference(set(parsed_query['exclude']))
|
| 116 |
+
for field in flat_fields:
|
| 117 |
+
flat_field = cls.build_flat_field(rec, field)
|
| 118 |
+
data.update(flat_field)
|
| 119 |
+
|
| 120 |
+
elif parsed_query["include"]:
|
| 121 |
+
# Here we are sure that self.parsed_restql_query["exclude"]
|
| 122 |
+
# is empty which means the exclude operator(-) is not used,
|
| 123 |
+
# so self.parsed_restql_query["include"] contains only fields
|
| 124 |
+
# to include
|
| 125 |
+
all_fields = rec.fields_get()
|
| 126 |
+
if "*" in parsed_query['include']:
|
| 127 |
+
# Include all fields
|
| 128 |
+
parsed_query['include'] = filter(
|
| 129 |
+
lambda item: item != "*",
|
| 130 |
+
parsed_query['include']
|
| 131 |
+
)
|
| 132 |
+
fields = chain(parsed_query['include'], all_fields)
|
| 133 |
+
parsed_query['include'] = list(fields)
|
| 134 |
+
|
| 135 |
+
for field in parsed_query["include"]:
|
| 136 |
+
if isinstance(field, dict):
|
| 137 |
+
for nested_field, nested_parsed_query in field.items():
|
| 138 |
+
built_nested_field = cls.build_nested_field(
|
| 139 |
+
rec,
|
| 140 |
+
nested_field,
|
| 141 |
+
nested_parsed_query
|
| 142 |
+
)
|
| 143 |
+
data.update(built_nested_field)
|
| 144 |
+
else:
|
| 145 |
+
flat_field = cls.build_flat_field(rec, field)
|
| 146 |
+
data.update(flat_field)
|
| 147 |
+
else:
|
| 148 |
+
# The query is empty i.e query={}
|
| 149 |
+
# return nothing
|
| 150 |
+
return {}
|
| 151 |
+
return data
|
rest-api/demo/demo.xml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<odoo>
|
| 2 |
+
<data>
|
| 3 |
+
|
| 4 |
+
</data>
|
| 5 |
+
</odoo>
|
rest-api/models/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
from . import models
|
rest-api/models/models.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
|
| 3 |
+
from odoo import models, fields, api
|
rest-api/security/ir.model.access.csv
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
rest-api/views/templates.xml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<odoo>
|
| 2 |
+
<data>
|
| 3 |
+
|
| 4 |
+
</data>
|
| 5 |
+
</odoo>
|
rest-api/views/views.xml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<odoo>
|
| 2 |
+
<data>
|
| 3 |
+
|
| 4 |
+
</data>
|
| 5 |
+
</odoo>
|