BinaryONe commited on
Commit
f9dacfc
·
1 Parent(s): b77888d

initial Commit

Browse files
.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>