PyOdoo 0.5.9__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/PKG-INFO +3 -1
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/PyOdoo.egg-info/PKG-INFO +3 -1
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/PyOdoo.egg-info/SOURCES.txt +5 -1
- PyOdoo-0.6.0/PyOdoo.egg-info/requires.txt +1 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/README.md +2 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/constants.py +1 -1
- PyOdoo-0.6.0/pyodoo/samples/sql_excel_query.py +81 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/v12/__init__.py +1 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/v12/model.py +14 -8
- PyOdoo-0.6.0/pyodoo/v12/sql_excel_query.py +310 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/setup.cfg +2 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/tests/test_contacts.py +26 -5
- PyOdoo-0.6.0/tests/test_sql_excel_query.py +233 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/LICENSE +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/PyOdoo.egg-info/dependency_links.txt +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/PyOdoo.egg-info/top_level.txt +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/__init__.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/active_status_choice.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/boolean_operator.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/compare_type.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/filter.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/message_subtype.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/samples/__init__.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/samples/awaitable.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/samples/contacts.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/samples/contacts_async.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyodoo/v12/api.py +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/pyproject.toml +0 -0
- {PyOdoo-0.5.9 → PyOdoo-0.6.0}/tests/test_compare_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PyOdoo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: API for Odoo
|
|
5
5
|
Home-page: http://www.muflone.com/pyodoo/
|
|
6
6
|
Author: Fabio Castelli
|
|
@@ -44,6 +44,8 @@ its XML RPC API.
|
|
|
44
44
|
# System Requirements
|
|
45
45
|
|
|
46
46
|
* Python 3.x
|
|
47
|
+
* xlrd 2.0.x (https://pypi.org/project/xlrd/)
|
|
48
|
+
* requests 2.31.x (https://pypi.org/project/requests/)
|
|
47
49
|
|
|
48
50
|
# Usage
|
|
49
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PyOdoo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: API for Odoo
|
|
5
5
|
Home-page: http://www.muflone.com/pyodoo/
|
|
6
6
|
Author: Fabio Castelli
|
|
@@ -44,6 +44,8 @@ its XML RPC API.
|
|
|
44
44
|
# System Requirements
|
|
45
45
|
|
|
46
46
|
* Python 3.x
|
|
47
|
+
* xlrd 2.0.x (https://pypi.org/project/xlrd/)
|
|
48
|
+
* requests 2.31.x (https://pypi.org/project/requests/)
|
|
47
49
|
|
|
48
50
|
# Usage
|
|
49
51
|
|
|
@@ -5,6 +5,7 @@ setup.cfg
|
|
|
5
5
|
PyOdoo.egg-info/PKG-INFO
|
|
6
6
|
PyOdoo.egg-info/SOURCES.txt
|
|
7
7
|
PyOdoo.egg-info/dependency_links.txt
|
|
8
|
+
PyOdoo.egg-info/requires.txt
|
|
8
9
|
PyOdoo.egg-info/top_level.txt
|
|
9
10
|
pyodoo/__init__.py
|
|
10
11
|
pyodoo/active_status_choice.py
|
|
@@ -17,8 +18,11 @@ pyodoo/samples/__init__.py
|
|
|
17
18
|
pyodoo/samples/awaitable.py
|
|
18
19
|
pyodoo/samples/contacts.py
|
|
19
20
|
pyodoo/samples/contacts_async.py
|
|
21
|
+
pyodoo/samples/sql_excel_query.py
|
|
20
22
|
pyodoo/v12/__init__.py
|
|
21
23
|
pyodoo/v12/api.py
|
|
22
24
|
pyodoo/v12/model.py
|
|
25
|
+
pyodoo/v12/sql_excel_query.py
|
|
23
26
|
tests/test_compare_types.py
|
|
24
|
-
tests/test_contacts.py
|
|
27
|
+
tests/test_contacts.py
|
|
28
|
+
tests/test_sql_excel_query.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xlrd==2.0.1
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Project: PyOdoo
|
|
3
|
+
# Description: API for Odoo
|
|
4
|
+
# Author: Fabio Castelli (Muflone) <muflone@muflone.com>
|
|
5
|
+
# Copyright: 2021-2023 Fabio Castelli
|
|
6
|
+
# License: GPL-3+
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU General Public License
|
|
18
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
from pyodoo.constants import APP_NAME, APP_VERSION
|
|
24
|
+
from pyodoo.v12 import SqlExcelQuery
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Check SQL Excel Query availability
|
|
28
|
+
print(SqlExcelQuery.is_available(endpoint=os.environ['ODOO_ENDPOINT'],
|
|
29
|
+
database=os.environ['ODOO_DATABASE'],
|
|
30
|
+
username=os.environ['ODOO_USERNAME'],
|
|
31
|
+
password=os.environ['ODOO_PASSWORD'],
|
|
32
|
+
language=None))
|
|
33
|
+
# Instance SQL Excel Query object
|
|
34
|
+
query = SqlExcelQuery(name='test query 1',
|
|
35
|
+
category=f'{APP_NAME} {APP_VERSION}',
|
|
36
|
+
endpoint=os.environ['ODOO_ENDPOINT'],
|
|
37
|
+
database=os.environ['ODOO_DATABASE'],
|
|
38
|
+
username=os.environ['ODOO_USERNAME'],
|
|
39
|
+
password=os.environ['ODOO_PASSWORD'],
|
|
40
|
+
language=None)
|
|
41
|
+
# Change default language
|
|
42
|
+
query.language = 'en_GB'
|
|
43
|
+
|
|
44
|
+
# Get Query ID
|
|
45
|
+
print(query.id)
|
|
46
|
+
|
|
47
|
+
# Set Query SQL text
|
|
48
|
+
print(query.set_sql(text='SELECT\n *\nFROM res_partner\nLIMIT 100'))
|
|
49
|
+
|
|
50
|
+
# Set and get Query state
|
|
51
|
+
query.set_draft()
|
|
52
|
+
print(query.get_state())
|
|
53
|
+
query.validate()
|
|
54
|
+
print(query.get_state())
|
|
55
|
+
|
|
56
|
+
# Set Query active status
|
|
57
|
+
print(query.set_active(active=False))
|
|
58
|
+
print(query.set_active(active=True))
|
|
59
|
+
|
|
60
|
+
# Execute the SQL query
|
|
61
|
+
query.execute()
|
|
62
|
+
|
|
63
|
+
# Get Excel file
|
|
64
|
+
data = query.get_file()
|
|
65
|
+
|
|
66
|
+
# Process the Excel file and extract its data
|
|
67
|
+
data = query.get_data()
|
|
68
|
+
print(data)
|
|
69
|
+
|
|
70
|
+
# Remove Excel file from SQL Excel Query
|
|
71
|
+
query.clear()
|
|
72
|
+
|
|
73
|
+
# Add query tags
|
|
74
|
+
query.add_tag(tag_id=4)
|
|
75
|
+
|
|
76
|
+
# Get the query tags
|
|
77
|
+
data = query.get_tags()
|
|
78
|
+
print(data)
|
|
79
|
+
|
|
80
|
+
# Delete SQL Excel Query object
|
|
81
|
+
print(query.delete())
|
|
@@ -92,20 +92,26 @@ class Model(object):
|
|
|
92
92
|
|
|
93
93
|
def get_model(self,
|
|
94
94
|
model_name: str,
|
|
95
|
-
authenticate: bool = False
|
|
95
|
+
authenticate: bool = False,
|
|
96
|
+
use_existing_uid: bool = False) -> 'Model':
|
|
96
97
|
"""
|
|
97
98
|
Get a Model object for another model name
|
|
98
99
|
:param model_name: Model name
|
|
99
100
|
:param authenticate: Automatically authenticate user
|
|
101
|
+
:param use_existing_uid: Use the existing UID if not authenticated
|
|
100
102
|
:return: Model object
|
|
101
103
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
model = Model(model_name=model_name,
|
|
105
|
+
endpoint=self.api.endpoint,
|
|
106
|
+
database=self.api.database,
|
|
107
|
+
username=self.api.username,
|
|
108
|
+
password=self.api.password,
|
|
109
|
+
language=self.api.language,
|
|
110
|
+
authenticate=authenticate)
|
|
111
|
+
if not authenticate and use_existing_uid:
|
|
112
|
+
# Use the existing UID
|
|
113
|
+
model.api.uid = self.api.uid
|
|
114
|
+
return model
|
|
109
115
|
|
|
110
116
|
def get_model_data_reference(self,
|
|
111
117
|
module_name: str,
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Project: PyOdoo
|
|
3
|
+
# Description: API for Odoo
|
|
4
|
+
# Author: Fabio Castelli (Muflone) <muflone@muflone.com>
|
|
5
|
+
# Copyright: 2021-2023 Fabio Castelli
|
|
6
|
+
# License: GPL-3+
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU General Public License
|
|
18
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import io
|
|
23
|
+
from typing import Any, Optional
|
|
24
|
+
|
|
25
|
+
import pyodoo
|
|
26
|
+
from pyodoo.v12.model import Model
|
|
27
|
+
|
|
28
|
+
import xlrd
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SqlExcelQuery(object):
|
|
32
|
+
"""
|
|
33
|
+
SQL Excel query to extract data from tables
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self,
|
|
36
|
+
name: str,
|
|
37
|
+
category: str,
|
|
38
|
+
endpoint: str,
|
|
39
|
+
database: str,
|
|
40
|
+
username: str,
|
|
41
|
+
password: str,
|
|
42
|
+
language: str):
|
|
43
|
+
# Model object
|
|
44
|
+
self._query_name = name
|
|
45
|
+
self.model = Model(model_name='sql.excel.pdf',
|
|
46
|
+
endpoint=endpoint,
|
|
47
|
+
database=database,
|
|
48
|
+
username=username,
|
|
49
|
+
password=password,
|
|
50
|
+
language=language,
|
|
51
|
+
authenticate=True)
|
|
52
|
+
# Get or create the category ID
|
|
53
|
+
model_category = self.model.get_model(model_name='sql.category',
|
|
54
|
+
authenticate=False,
|
|
55
|
+
use_existing_uid=True)
|
|
56
|
+
results = model_category.search(
|
|
57
|
+
filters=[pyodoo.Filter(field='name',
|
|
58
|
+
compare_type=pyodoo.CompareType.EQUAL,
|
|
59
|
+
value=category)],
|
|
60
|
+
limit=1)
|
|
61
|
+
if results:
|
|
62
|
+
category_id = results[0]
|
|
63
|
+
else:
|
|
64
|
+
category_id = model_category.create(values={'name': category})
|
|
65
|
+
# Get or create the query object
|
|
66
|
+
results = self.model.search(
|
|
67
|
+
filters=[pyodoo.Filter(field='name',
|
|
68
|
+
compare_type=pyodoo.CompareType.EQUAL,
|
|
69
|
+
value=self.name)],
|
|
70
|
+
is_active=pyodoo.ActiveStatusChoice.BOTH,
|
|
71
|
+
limit=1)
|
|
72
|
+
if results:
|
|
73
|
+
self._query_id = results[0]
|
|
74
|
+
else:
|
|
75
|
+
# Query object doesn't exist, create it right now
|
|
76
|
+
self._query_id = self.model.create(
|
|
77
|
+
values={'name': self.name,
|
|
78
|
+
'category_id': category_id,
|
|
79
|
+
'sql_query': ''})
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def is_available(endpoint: str,
|
|
83
|
+
database: str,
|
|
84
|
+
username: str,
|
|
85
|
+
password: str,
|
|
86
|
+
language: str) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Check if the sql.excel.pdf model exists
|
|
89
|
+
|
|
90
|
+
:param endpoint: Odoo instance endpoint
|
|
91
|
+
:param database: Odoo database
|
|
92
|
+
:param username: Odoo username
|
|
93
|
+
:param password: User password
|
|
94
|
+
:param language: Language for localizations
|
|
95
|
+
|
|
96
|
+
:return: True if the model sql.excel.pdf exists
|
|
97
|
+
"""
|
|
98
|
+
model = Model(model_name='ir.model',
|
|
99
|
+
endpoint=endpoint,
|
|
100
|
+
database=database,
|
|
101
|
+
username=username,
|
|
102
|
+
password=password,
|
|
103
|
+
language=language,
|
|
104
|
+
authenticate=True)
|
|
105
|
+
results = model.search(
|
|
106
|
+
filters=[pyodoo.Filter(field='model',
|
|
107
|
+
compare_type=pyodoo.CompareType.EQUAL,
|
|
108
|
+
value='sql.excel.pdf')],
|
|
109
|
+
limit=1)
|
|
110
|
+
return bool(results)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def name(self) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Get the current query name
|
|
116
|
+
|
|
117
|
+
:return: Query name
|
|
118
|
+
"""
|
|
119
|
+
return self._query_name
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def language(self) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Get the current default language
|
|
125
|
+
|
|
126
|
+
:return: Language code
|
|
127
|
+
"""
|
|
128
|
+
return self.model.language
|
|
129
|
+
|
|
130
|
+
@language.setter
|
|
131
|
+
def language(self,
|
|
132
|
+
language: str):
|
|
133
|
+
"""
|
|
134
|
+
Set the current default language
|
|
135
|
+
|
|
136
|
+
:param language: Language code to set
|
|
137
|
+
"""
|
|
138
|
+
self.model.language = language
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def id(self) -> int:
|
|
142
|
+
"""
|
|
143
|
+
Get the query object ID
|
|
144
|
+
:return: query object ID
|
|
145
|
+
"""
|
|
146
|
+
return self._query_id
|
|
147
|
+
|
|
148
|
+
def get_sql(self) -> Optional[str]:
|
|
149
|
+
"""
|
|
150
|
+
Get the query SQL text
|
|
151
|
+
|
|
152
|
+
:return: SQL text for the query
|
|
153
|
+
"""
|
|
154
|
+
results = self.model.get(entity_id=self._query_id,
|
|
155
|
+
fields=('id',
|
|
156
|
+
'sql_query'))
|
|
157
|
+
return results['sql_query'] if results else None
|
|
158
|
+
|
|
159
|
+
def set_sql(self, text: str) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
Set the query SQL text
|
|
162
|
+
|
|
163
|
+
:param text: SQL text for query
|
|
164
|
+
:return: True if the query was updated
|
|
165
|
+
"""
|
|
166
|
+
return self.model.update(entity_id=self._query_id,
|
|
167
|
+
values={'sql_query': text})
|
|
168
|
+
|
|
169
|
+
def set_draft(self) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Set the query object as draft
|
|
172
|
+
"""
|
|
173
|
+
self.model.execute(method_name='action_reset_draft',
|
|
174
|
+
args=[self._query_id],
|
|
175
|
+
kwargs={},
|
|
176
|
+
ignore_none_errors=True)
|
|
177
|
+
|
|
178
|
+
def validate(self) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Validate the query object
|
|
181
|
+
"""
|
|
182
|
+
self.model.execute(method_name='action_validate',
|
|
183
|
+
args=[self._query_id],
|
|
184
|
+
kwargs={},
|
|
185
|
+
ignore_none_errors=False)
|
|
186
|
+
|
|
187
|
+
def get_state(self) -> Optional[str]:
|
|
188
|
+
"""
|
|
189
|
+
Get the query object state
|
|
190
|
+
|
|
191
|
+
:return: the query object state
|
|
192
|
+
"""
|
|
193
|
+
results = self.model.get(entity_id=self._query_id,
|
|
194
|
+
fields=('state', ))
|
|
195
|
+
return results['state'] if results else None
|
|
196
|
+
|
|
197
|
+
def get_active(self) -> Optional[str]:
|
|
198
|
+
"""
|
|
199
|
+
Get the query object active state
|
|
200
|
+
|
|
201
|
+
:return: the query object active state
|
|
202
|
+
"""
|
|
203
|
+
results = self.model.get(entity_id=self._query_id,
|
|
204
|
+
fields=('active', ))
|
|
205
|
+
return results['active'] if results else None
|
|
206
|
+
|
|
207
|
+
def set_active(self, active: bool) -> bool:
|
|
208
|
+
"""
|
|
209
|
+
Set the query object as active/inactive
|
|
210
|
+
|
|
211
|
+
:param active: active status
|
|
212
|
+
:return: True if the query was updated
|
|
213
|
+
"""
|
|
214
|
+
return self.model.update(entity_id=self._query_id,
|
|
215
|
+
values={'active': active})
|
|
216
|
+
|
|
217
|
+
def execute(self) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Execute the query to create the Excel file in the `file` field
|
|
220
|
+
|
|
221
|
+
:return: None
|
|
222
|
+
"""
|
|
223
|
+
self.model.execute(method_name='print_xls_report',
|
|
224
|
+
args=[self._query_id],
|
|
225
|
+
kwargs={},
|
|
226
|
+
ignore_none_errors=True)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
def clear(self) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Clear the produced file
|
|
232
|
+
"""
|
|
233
|
+
self.model.update(entity_id=self._query_id,
|
|
234
|
+
values={'file': False})
|
|
235
|
+
|
|
236
|
+
def delete(self) -> bool:
|
|
237
|
+
"""
|
|
238
|
+
Delete the query object
|
|
239
|
+
|
|
240
|
+
:return: True if the query was removed
|
|
241
|
+
"""
|
|
242
|
+
return self.model.delete(entity_id=self._query_id)
|
|
243
|
+
|
|
244
|
+
def get_file(self) -> Optional[str]:
|
|
245
|
+
"""
|
|
246
|
+
Get the latest produced Excel file
|
|
247
|
+
|
|
248
|
+
:return: Excel binary file content
|
|
249
|
+
"""
|
|
250
|
+
results = self.model.get(entity_id=self._query_id,
|
|
251
|
+
fields=('file', ))
|
|
252
|
+
if results and results['file']:
|
|
253
|
+
# Decode the base64 content
|
|
254
|
+
results = base64.b64decode(s=results['file'])
|
|
255
|
+
else:
|
|
256
|
+
results = None
|
|
257
|
+
return results
|
|
258
|
+
|
|
259
|
+
def get_data(self) -> Optional[list[dict[str, Any]]]:
|
|
260
|
+
"""
|
|
261
|
+
Get the latest produced Excel file and extract its tabular data
|
|
262
|
+
|
|
263
|
+
:return: list of dictionaries with data
|
|
264
|
+
"""
|
|
265
|
+
with io.BytesIO() as file:
|
|
266
|
+
file.write(self.get_file())
|
|
267
|
+
with xlrd.open_workbook(file_contents=file.getvalue()) as workbook:
|
|
268
|
+
worksheet = workbook.sheet_by_index(0)
|
|
269
|
+
rows = worksheet.get_rows()
|
|
270
|
+
# Skip the first three rows
|
|
271
|
+
next(rows)
|
|
272
|
+
next(rows)
|
|
273
|
+
next(rows)
|
|
274
|
+
# Get the column headers
|
|
275
|
+
headers = [item.value
|
|
276
|
+
for item in next(rows)]
|
|
277
|
+
# Get the data
|
|
278
|
+
results = []
|
|
279
|
+
for row in rows:
|
|
280
|
+
results.append(dict(zip(headers,
|
|
281
|
+
[item.value for item in row])))
|
|
282
|
+
return results
|
|
283
|
+
|
|
284
|
+
def add_tag(self, tag_id: int) -> bool:
|
|
285
|
+
"""
|
|
286
|
+
Add an existing tag to the query
|
|
287
|
+
|
|
288
|
+
:param tag_id: Tag ID to add
|
|
289
|
+
:return: True if the tag was added
|
|
290
|
+
"""
|
|
291
|
+
return self.model.many_to_many_add(entity_id=self._query_id,
|
|
292
|
+
field='tag_ids',
|
|
293
|
+
related_id=tag_id)
|
|
294
|
+
|
|
295
|
+
def get_tags(self) -> Optional[list[dict[str, Any]]]:
|
|
296
|
+
"""
|
|
297
|
+
Get the query tags
|
|
298
|
+
|
|
299
|
+
:return: list with the tags
|
|
300
|
+
"""
|
|
301
|
+
if results := self.model.get(entity_id=self._query_id,
|
|
302
|
+
fields=('id',
|
|
303
|
+
'tag_ids')):
|
|
304
|
+
model = self.model.get_model(model_name='sql.tags',
|
|
305
|
+
authenticate=False,
|
|
306
|
+
use_existing_uid=True)
|
|
307
|
+
results = model.get_many(entity_ids=results['tag_ids'],
|
|
308
|
+
fields=('id',
|
|
309
|
+
'name'))
|
|
310
|
+
return results or None
|
|
@@ -784,14 +784,35 @@ class TestCaseContacts(unittest.TestCase):
|
|
|
784
784
|
self.assertIn('string', field_name)
|
|
785
785
|
self.assertIn('type', field_name)
|
|
786
786
|
|
|
787
|
-
def
|
|
787
|
+
def test_41_get_model_authenticate(self) -> None:
|
|
788
788
|
"""
|
|
789
789
|
Get a new Model object
|
|
790
790
|
"""
|
|
791
791
|
model_name = 'res.users'
|
|
792
|
-
|
|
793
|
-
|
|
792
|
+
model = self.model.get_model(model_name=model_name,
|
|
793
|
+
authenticate=True,
|
|
794
|
+
use_existing_uid=False)
|
|
795
|
+
results = model.count(filters=[])
|
|
794
796
|
# Check if we have results
|
|
797
|
+
self.assertIsNotNone(model)
|
|
798
|
+
self.assertIsInstance(model, Model)
|
|
799
|
+
self.assertEqual(model.model_name, model_name)
|
|
795
800
|
self.assertIsNotNone(results)
|
|
796
|
-
self.
|
|
797
|
-
|
|
801
|
+
self.assertGreater(results, 0)
|
|
802
|
+
|
|
803
|
+
def test_42_get_model_no_authenticate(self) -> None:
|
|
804
|
+
"""
|
|
805
|
+
Get a new Model object using the existing UID
|
|
806
|
+
"""
|
|
807
|
+
self.model.authenticate()
|
|
808
|
+
model_name = 'res.users'
|
|
809
|
+
model = self.model.get_model(model_name=model_name,
|
|
810
|
+
authenticate=False,
|
|
811
|
+
use_existing_uid=True)
|
|
812
|
+
results = model.count(filters=[])
|
|
813
|
+
# Check if we have results
|
|
814
|
+
self.assertIsNotNone(model)
|
|
815
|
+
self.assertIsInstance(model, Model)
|
|
816
|
+
self.assertEqual(model.model_name, model_name)
|
|
817
|
+
self.assertIsNotNone(results)
|
|
818
|
+
self.assertGreater(results, 0)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Project: PyOdoo
|
|
3
|
+
# Description: API for Odoo
|
|
4
|
+
# Author: Fabio Castelli (Muflone) <muflone@muflone.com>
|
|
5
|
+
# Copyright: 2021-2023 Fabio Castelli
|
|
6
|
+
# License: GPL-3+
|
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU General Public License
|
|
18
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
import unittest
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
from pyodoo.constants import APP_NAME, APP_VERSION
|
|
25
|
+
from pyodoo.v12 import SqlExcelQuery
|
|
26
|
+
|
|
27
|
+
import utility
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestCaseSqlExcelQuery(unittest.TestCase):
|
|
31
|
+
@classmethod
|
|
32
|
+
def setUpClass(cls):
|
|
33
|
+
"""
|
|
34
|
+
Query object preparation
|
|
35
|
+
"""
|
|
36
|
+
info = utility.get_authentication_from_demo()
|
|
37
|
+
if SqlExcelQuery.is_available(endpoint=info['host'],
|
|
38
|
+
database=info['database'],
|
|
39
|
+
username=info['user'],
|
|
40
|
+
password=info['password'],
|
|
41
|
+
language='en_US'):
|
|
42
|
+
# The model sql.excel.pdf is available
|
|
43
|
+
cls.query_name = uuid.uuid4().hex
|
|
44
|
+
cls.query = SqlExcelQuery(name=cls.query_name,
|
|
45
|
+
category=f'{APP_NAME} {APP_VERSION}',
|
|
46
|
+
endpoint=info['host'],
|
|
47
|
+
database=info['database'],
|
|
48
|
+
username=info['user'],
|
|
49
|
+
password=info['password'],
|
|
50
|
+
language='en_US')
|
|
51
|
+
else:
|
|
52
|
+
# The model sql.excel.pdf is not available
|
|
53
|
+
cls.query = None
|
|
54
|
+
|
|
55
|
+
def setUp(self):
|
|
56
|
+
"""
|
|
57
|
+
Check if the test could be run
|
|
58
|
+
"""
|
|
59
|
+
if not self.query:
|
|
60
|
+
self.skipTest('Model sql.excel.pdf is not available')
|
|
61
|
+
|
|
62
|
+
def test_01_get_name(self) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Check the query name
|
|
65
|
+
"""
|
|
66
|
+
results = self.query.name
|
|
67
|
+
# Check if the name is not None
|
|
68
|
+
self.assertIsNotNone(results)
|
|
69
|
+
# Check if the name is the same
|
|
70
|
+
self.assertEqual(results, self.query_name)
|
|
71
|
+
|
|
72
|
+
def test_02_get_language(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Check the query language
|
|
75
|
+
"""
|
|
76
|
+
results = self.query.language
|
|
77
|
+
# Check if the language is not None
|
|
78
|
+
self.assertIsNotNone(results)
|
|
79
|
+
# Check if the language is the same
|
|
80
|
+
self.assertEqual(results, 'en_US')
|
|
81
|
+
|
|
82
|
+
def test_03_get_id(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Check the query ID
|
|
85
|
+
"""
|
|
86
|
+
results = self.query.id
|
|
87
|
+
# Check if the ID is not None
|
|
88
|
+
self.assertIsNotNone(results)
|
|
89
|
+
# Check if the ID is > 0
|
|
90
|
+
self.assertGreater(results, 0)
|
|
91
|
+
|
|
92
|
+
def test_04_get_set_sql(self) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Get the query SQL text
|
|
95
|
+
"""
|
|
96
|
+
results = self.query.get_sql()
|
|
97
|
+
# Check if the query SQL text is not None
|
|
98
|
+
self.assertIsNotNone(results)
|
|
99
|
+
# Check if the query SQL text length is = 0
|
|
100
|
+
self.assertEqual(len(results), 0)
|
|
101
|
+
# Set new query SQL text
|
|
102
|
+
sql_text = ('SELECT customer, supplier, COUNT(*) AS count '
|
|
103
|
+
'FROM res_partner '
|
|
104
|
+
'GROUP BY customer, supplier')
|
|
105
|
+
results = self.query.set_sql(text=sql_text)
|
|
106
|
+
# Check if the response is True
|
|
107
|
+
self.assertEqual(results, True)
|
|
108
|
+
# Check if the query SQL text is not None
|
|
109
|
+
results = self.query.get_sql()
|
|
110
|
+
self.assertIsNotNone(results)
|
|
111
|
+
# Check if the query SQL text is the expected
|
|
112
|
+
self.assertEqual(results, sql_text)
|
|
113
|
+
|
|
114
|
+
def test_05_get_state(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Get the query state
|
|
117
|
+
"""
|
|
118
|
+
results = self.query.get_state()
|
|
119
|
+
# Check if the query state is not None
|
|
120
|
+
self.assertIsNotNone(results)
|
|
121
|
+
# Check if the query state is draft
|
|
122
|
+
self.assertEqual(results, 'draft')
|
|
123
|
+
|
|
124
|
+
def test_06_validate(self) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Set the query state to validate
|
|
127
|
+
"""
|
|
128
|
+
self.query.validate()
|
|
129
|
+
# Check if the query state is not None
|
|
130
|
+
results = self.query.get_state()
|
|
131
|
+
self.assertIsNotNone(results)
|
|
132
|
+
# Check if the query state is validate
|
|
133
|
+
self.assertEqual(results, 'validate')
|
|
134
|
+
|
|
135
|
+
def test_07_set_draft(self) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Set the query state to draft
|
|
138
|
+
"""
|
|
139
|
+
self.query.set_draft()
|
|
140
|
+
# Check if the query state is not None
|
|
141
|
+
results = self.query.get_state()
|
|
142
|
+
self.assertIsNotNone(results)
|
|
143
|
+
# Check if the query state is draft
|
|
144
|
+
self.assertEqual(results, 'draft')
|
|
145
|
+
|
|
146
|
+
def test_08_get_set_active(self) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Get and set the query active state
|
|
149
|
+
"""
|
|
150
|
+
# Set the query as not active
|
|
151
|
+
results = self.query.set_active(active=False)
|
|
152
|
+
self.assertTrue(results)
|
|
153
|
+
# Check if the query active state is False
|
|
154
|
+
results = self.query.get_active()
|
|
155
|
+
self.assertFalse(results)
|
|
156
|
+
# Set the query as active
|
|
157
|
+
results = self.query.set_active(active=True)
|
|
158
|
+
self.assertTrue(results)
|
|
159
|
+
# Check if the query active state is True
|
|
160
|
+
results = self.query.get_active()
|
|
161
|
+
self.assertTrue(results)
|
|
162
|
+
|
|
163
|
+
def test_09_execute(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Execute the query
|
|
166
|
+
"""
|
|
167
|
+
results = self.query.execute()
|
|
168
|
+
# Check if the query was executed
|
|
169
|
+
self.assertIsNone(results)
|
|
170
|
+
|
|
171
|
+
def test_10_get_file(self) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Get the query execution results as file
|
|
174
|
+
"""
|
|
175
|
+
results = self.query.get_file()
|
|
176
|
+
# Check if the file is not None
|
|
177
|
+
self.assertIsNotNone(results)
|
|
178
|
+
# Check if the file is not empty
|
|
179
|
+
self.assertGreater(len(results), 0)
|
|
180
|
+
|
|
181
|
+
def test_11_clear(self) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Clear the query execution results file
|
|
184
|
+
"""
|
|
185
|
+
self.query.clear()
|
|
186
|
+
results = self.query.get_file()
|
|
187
|
+
# Check if the language is None
|
|
188
|
+
self.assertIsNone(results)
|
|
189
|
+
|
|
190
|
+
def test_12_get_data(self) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Get the query execution results as data
|
|
193
|
+
"""
|
|
194
|
+
self.query.execute()
|
|
195
|
+
results = self.query.get_data()
|
|
196
|
+
# Check if the results data are not None
|
|
197
|
+
self.assertIsNotNone(results)
|
|
198
|
+
# Check if the results have one or more results
|
|
199
|
+
self.assertGreaterEqual(len(results), 1)
|
|
200
|
+
# Check if the results data have the expected fields
|
|
201
|
+
self.assertIn('S.No', results[0])
|
|
202
|
+
self.assertIn('customer', results[0])
|
|
203
|
+
self.assertIn('supplier', results[0])
|
|
204
|
+
self.assertIn('count', results[0])
|
|
205
|
+
self.assertEqual(len(results[0]), 4)
|
|
206
|
+
|
|
207
|
+
def test_13_add_tags(self) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Add and get tags from the Query
|
|
210
|
+
"""
|
|
211
|
+
model = self.query.model.get_model(model_name='sql.tags',
|
|
212
|
+
authenticate=False,
|
|
213
|
+
use_existing_uid=True)
|
|
214
|
+
tag_ids = model.search(filters=[])
|
|
215
|
+
for tag_id in tag_ids:
|
|
216
|
+
results = self.query.add_tag(tag_id=tag_id)
|
|
217
|
+
# Check if the addition was successful
|
|
218
|
+
self.assertTrue(results)
|
|
219
|
+
# Check the resulting tags
|
|
220
|
+
results = self.query.get_tags()
|
|
221
|
+
# Check the tags list
|
|
222
|
+
self.assertIsNotNone(results)
|
|
223
|
+
results = [item['id'] for item in results]
|
|
224
|
+
for tag_id in tag_ids:
|
|
225
|
+
self.assertIn(tag_id, results)
|
|
226
|
+
|
|
227
|
+
def test_14_delete(self) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Delete the query
|
|
230
|
+
"""
|
|
231
|
+
results = self.query.delete()
|
|
232
|
+
# Check if the deletion was successful
|
|
233
|
+
self.assertTrue(results)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|