Spaces:
Sleeping
Sleeping
Commit
·
82e652d
1
Parent(s):
8e52d5a
mcp
Browse files- app.py +449 -4
- requirements.txt +4 -0
app.py
CHANGED
@@ -1,7 +1,452 @@
|
|
|
|
1 |
import gradio as gr
|
|
|
|
|
|
|
2 |
|
3 |
-
|
4 |
-
|
5 |
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import xmlrpc.client
|
2 |
import gradio as gr
|
3 |
+
import os
|
4 |
+
import json # For parsing args/kwargs in execute_kw
|
5 |
+
from dotenv import load_dotenv
|
6 |
|
7 |
+
# Load environment variables from .env file
|
8 |
+
load_dotenv()
|
9 |
|
10 |
+
# --- Odoo XML-RPC Helper Functions ---
|
11 |
+
def authenticate(url, db, username, password):
|
12 |
+
"""
|
13 |
+
Authenticate with Odoo using XML-RPC.
|
14 |
+
Returns user ID (int) or False.
|
15 |
+
"""
|
16 |
+
try:
|
17 |
+
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
|
18 |
+
uid = common.authenticate(db, username, password, {})
|
19 |
+
return uid if uid else False
|
20 |
+
except Exception as e:
|
21 |
+
print(f"Authentication error: {e}")
|
22 |
+
return False
|
23 |
+
|
24 |
+
def list_models(url, db, uid, password):
|
25 |
+
"""
|
26 |
+
Returns list of (display_name (technical_name), technical_name) tuples.
|
27 |
+
"""
|
28 |
+
try:
|
29 |
+
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
30 |
+
model_data = models_proxy.execute_kw(db, uid, password, 'ir.model', 'search_read',
|
31 |
+
[[]], {'fields': ['model', 'name'], 'order': 'name'})
|
32 |
+
return sorted([(f"{m['name']} ({m['model']})", m['model']) for m in model_data if m.get('model') and m.get('name')], key=lambda x: x[0])
|
33 |
+
except Exception as e:
|
34 |
+
print(f"List models error: {e}")
|
35 |
+
return []
|
36 |
+
|
37 |
+
def get_model_fields(url, db, uid, password, model_name):
|
38 |
+
"""
|
39 |
+
Returns dictionary of fields for a model.
|
40 |
+
"""
|
41 |
+
try:
|
42 |
+
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
43 |
+
fields = models_proxy.execute_kw(db, uid, password, model_name, 'fields_get', [],
|
44 |
+
{'attributes': ['string', 'type', 'help', 'readonly', 'required', 'relation', 'selection']})
|
45 |
+
return fields
|
46 |
+
except Exception as e:
|
47 |
+
print(f"Get model fields error for {model_name}: {e}")
|
48 |
+
return {"error": f"Failed to fetch fields for {model_name}: {str(e)}"}
|
49 |
+
|
50 |
+
def search_read(url, db, uid, password, model_name, domain, limit):
|
51 |
+
"""
|
52 |
+
Returns list of records matching domain and limit.
|
53 |
+
"""
|
54 |
+
try:
|
55 |
+
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
56 |
+
records = models_proxy.execute_kw(db, uid, password, model_name, 'search_read',
|
57 |
+
[domain], {'limit': limit}) # Removed 'raise_exception'
|
58 |
+
return records
|
59 |
+
except Exception as e:
|
60 |
+
print(f"Search read error for {model_name}: {e}")
|
61 |
+
return {"error": f"Failed to search_read {model_name}: {str(e)}"}
|
62 |
+
|
63 |
+
def search(url, db, uid, password, model_name, domain):
|
64 |
+
"""
|
65 |
+
Returns list of record IDs matching domain.
|
66 |
+
"""
|
67 |
+
try:
|
68 |
+
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
69 |
+
record_ids = models_proxy.execute_kw(db, uid, password, model_name, 'search',
|
70 |
+
[domain]) # Removed 'raise_exception'
|
71 |
+
return record_ids
|
72 |
+
except Exception as e:
|
73 |
+
print(f"Search error for {model_name}: {e}")
|
74 |
+
return {"error": f"Failed to search {model_name}: {str(e)}"}
|
75 |
+
|
76 |
+
def read(url, db, uid, password, model_name, record_id):
|
77 |
+
"""
|
78 |
+
Returns complete record details for a given ID.
|
79 |
+
"""
|
80 |
+
try:
|
81 |
+
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
82 |
+
# Odoo's read method expects a list of IDs, even if it's just one.
|
83 |
+
record = models_proxy.execute_kw(db, uid, password, model_name, 'read', [[record_id]]) # Removed 'raise_exception'
|
84 |
+
return record[0] if record else {"error": f"Record ID {record_id} not found or access denied."}
|
85 |
+
except Exception as e:
|
86 |
+
print(f"Read error for {model_name} ID {record_id}: {e}")
|
87 |
+
return {"error": f"Failed to read {model_name} ID {record_id}: {str(e)}"}
|
88 |
+
|
89 |
+
def execute_kw(url, db, uid, password, model_name, method, args, kwargs):
|
90 |
+
"""
|
91 |
+
Execute a generic method on a model.
|
92 |
+
Filters out 'raise_exception' for methods that do not support it (e.g., create, write).
|
93 |
+
"""
|
94 |
+
try:
|
95 |
+
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
96 |
+
# Only allow 'raise_exception' for methods that support it
|
97 |
+
methods_with_raise_exception = ["search", "search_read", "read"]
|
98 |
+
if method not in methods_with_raise_exception:
|
99 |
+
# Remove 'raise_exception' if present
|
100 |
+
kwargs.pop('raise_exception', None)
|
101 |
+
result = models_proxy.execute_kw(db, uid, password, model_name, method, args, kwargs)
|
102 |
+
return result
|
103 |
+
except Exception as e:
|
104 |
+
print(f"Execution error for {model_name}.{method}: {e}")
|
105 |
+
return {"error": f"Failed to execute {method} on {model_name}: {str(e)}"}
|
106 |
+
|
107 |
+
# --- Gradio Application with gr.Blocks ---
|
108 |
+
with gr.Blocks(title="Odoo Inspector", theme=gr.themes.Soft()) as demo:
|
109 |
+
# State variables
|
110 |
+
state_uid = gr.State(None)
|
111 |
+
state_odoo_url = gr.State(None)
|
112 |
+
state_odoo_db = gr.State(None)
|
113 |
+
state_odoo_password = gr.State(None)
|
114 |
+
|
115 |
+
gr.Markdown("## 1. Odoo Connection Details")
|
116 |
+
with gr.Row():
|
117 |
+
input_odoo_url = gr.Textbox(label="Odoo URL", value=os.getenv("ODOO_URL", ""), interactive=True)
|
118 |
+
input_odoo_db = gr.Textbox(label="Database Name", value=os.getenv("ODOO_DB", ""), interactive=True)
|
119 |
+
with gr.Row():
|
120 |
+
input_odoo_username = gr.Textbox(label="Username", value=os.getenv("ODOO_USERNAME", ""), interactive=True)
|
121 |
+
input_odoo_password = gr.Textbox(label="Password", value=os.getenv("ODOO_PASSWORD", ""), interactive=True)
|
122 |
+
connect_button = gr.Button("Connect and Fetch Models")
|
123 |
+
connection_status_output = gr.Textbox(label="Status", interactive=False)
|
124 |
+
|
125 |
+
with gr.Group(visible=False) as model_inspector_group:
|
126 |
+
gr.Markdown("## 2. Inspect Model Fields")
|
127 |
+
# The choices for model_dropdown will be dynamically updated by the connection step
|
128 |
+
model_dropdown = gr.Dropdown(label="Select a Model", choices=[], interactive=True, allow_custom_value=True)
|
129 |
+
model_name_api = gr.Textbox(label="Model Name (API)", visible=False)
|
130 |
+
get_fields_button = gr.Button("Get Fields for Selected Model")
|
131 |
+
get_fields_api_button = gr.Button(visible=False) # Dummy button for MCP binding
|
132 |
+
model_fields_output = gr.JSON(label="Model Fields")
|
133 |
+
|
134 |
+
with gr.Group(visible=False) as record_ops_group:
|
135 |
+
gr.Markdown("## 3. Record Operations")
|
136 |
+
gr.Markdown("""
|
137 |
+
Use this section to find and view records from the selected Odoo model.
|
138 |
+
|
139 |
+
**Domain**: Specify search criteria as a Python list of lists/tuples.
|
140 |
+
- Example: `[['is_company', '=', True], ['customer_rank', '>', 0]]` (finds companies that are customers).
|
141 |
+
- Use `[]` to match all records (respecting Odoo's default limits if any).
|
142 |
+
- The input is evaluated as Python code, so ensure correct syntax.
|
143 |
+
|
144 |
+
**Limit**: Max number of records for 'Search & Read Records'.
|
145 |
+
|
146 |
+
**Buttons**:
|
147 |
+
- `Search & Read Records`: Fetches full records matching the domain and limit.
|
148 |
+
- `Search Record IDs`: Fetches only the IDs of records matching the domain. Populates the 'Select Record ID' dropdown.
|
149 |
+
- `Read Selected Record Details`: Fetches full details for the ID chosen in 'Select Record ID'.
|
150 |
+
|
151 |
+
**Select Record ID**: Choose an ID from the list populated by `Search Record IDs`, or type a specific record ID and press Enter/Return before clicking `Read Selected Record Details`.
|
152 |
+
""")
|
153 |
+
domain_input = gr.Textbox(label="Domain (Python list of lists/tuples)", placeholder="[['field_name', '=', 'value']] or [] for all", value="[]")
|
154 |
+
limit_input = gr.Number(label="Limit (for Search & Read)", value=10, minimum=1, step=1)
|
155 |
+
with gr.Row():
|
156 |
+
search_read_button = gr.Button("Search & Read Records")
|
157 |
+
search_button = gr.Button("Search Record IDs")
|
158 |
+
record_id_dropdown = gr.Dropdown(label="Select Record ID (from Search results or type ID)", choices=[], interactive=True, allow_custom_value=True)
|
159 |
+
read_button = gr.Button("Read Selected Record Details")
|
160 |
+
record_operation_output = gr.JSON(label="Record Operation Result")
|
161 |
+
|
162 |
+
with gr.Group(visible=False) as exec_kw_group:
|
163 |
+
gr.Markdown("## 4. Execute Generic Odoo Method")
|
164 |
+
gr.Markdown("""
|
165 |
+
This section allows you to call almost any method on the selected Odoo model via `execute_kw`.
|
166 |
+
|
167 |
+
**Method Name**: The technical name of the method to call (e.g., `check_access_rights`, `name_search`, `default_get`).
|
168 |
+
|
169 |
+
**Arguments (JSON list)**: Positional arguments for the method, provided as a JSON-formatted list.
|
170 |
+
- Example for `check_access_rights`: `["read", false]`
|
171 |
+
- Example for `name_search`: `[]` (if using named arguments in Kwargs for criteria)
|
172 |
+
- If no positional arguments, use `[]`.
|
173 |
+
|
174 |
+
**Keyword Arguments (JSON dict)**: Named arguments for the method, provided as a JSON-formatted dictionary.
|
175 |
+
- Example for `check_access_rights`: `{"raise_exception": false}`
|
176 |
+
- Example for `name_search`: `{"name": "Azure", "operator": "ilike", "limit": 8}`
|
177 |
+
- It's often useful to include `"raise_exception": false` to prevent Odoo from stopping the XML-RPC call on application errors, allowing the app to display the error instead.
|
178 |
+
- For context: `{"context": {"lang": "en_US", "active_test": false}}`
|
179 |
+
- If no keyword arguments, use `{}`.
|
180 |
+
""")
|
181 |
+
method_name_input = gr.Textbox(label="Method Name", placeholder="e.g., check_access_rights")
|
182 |
+
args_input = gr.Textbox(label="Arguments (JSON list)", placeholder='[\"read\", false]', value="[]", lines=2)
|
183 |
+
kwargs_input = gr.Textbox(label="Keyword Arguments (JSON dict)", placeholder='{"raise_exception": false, "context": {"lang": "en_US"}}', value="{}", lines=3)
|
184 |
+
execute_kw_button = gr.Button("Execute Method")
|
185 |
+
execute_kw_output = gr.JSON(label="Method Execution Result")
|
186 |
+
|
187 |
+
# --- Event Handlers ---
|
188 |
+
def handle_connect_and_fetch_models():
|
189 |
+
"""
|
190 |
+
Establishes a connection to the Odoo server and fetches available models for inspection using credentials from environment variables.
|
191 |
+
Returns: list: UI updates including connection status and available models.
|
192 |
+
"""
|
193 |
+
url = os.environ.get("ODOO_URL")
|
194 |
+
db = os.environ.get("ODOO_DB")
|
195 |
+
username = os.environ.get("ODOO_USERNAME")
|
196 |
+
password = os.environ.get("ODOO_PASSWORD")
|
197 |
+
uid_val = authenticate(url, db, username, password)
|
198 |
+
updates = {
|
199 |
+
connection_status_output: "",
|
200 |
+
model_dropdown: gr.update(choices=[], value=None),
|
201 |
+
model_inspector_group: gr.update(visible=False),
|
202 |
+
record_ops_group: gr.update(visible=False),
|
203 |
+
exec_kw_group: gr.update(visible=False)
|
204 |
+
}
|
205 |
+
if not uid_val:
|
206 |
+
updates[connection_status_output] = "Authentication failed. Check credentials and Odoo connection."
|
207 |
+
return list(updates.values())
|
208 |
+
|
209 |
+
models_choices = list_models(url, db, uid_val, password)
|
210 |
+
if not models_choices:
|
211 |
+
updates[connection_status_output] = f"Authenticated (UID: {uid_val}), but failed to fetch models or no models found."
|
212 |
+
return list(updates.values())
|
213 |
+
|
214 |
+
updates[connection_status_output] = f"Successfully connected. UID: {uid_val}. Models loaded."
|
215 |
+
updates[model_dropdown] = gr.update(choices=models_choices, value=models_choices[0][1] if models_choices else None, interactive=True)
|
216 |
+
updates[model_inspector_group] = gr.update(visible=True)
|
217 |
+
updates[record_ops_group] = gr.update(visible=True)
|
218 |
+
updates[exec_kw_group] = gr.update(visible=True)
|
219 |
+
return list(updates.values())
|
220 |
+
|
221 |
+
connect_button.click(
|
222 |
+
fn=handle_connect_and_fetch_models,
|
223 |
+
inputs=[],
|
224 |
+
outputs=[connection_status_output, model_dropdown, model_inspector_group, record_ops_group, exec_kw_group],
|
225 |
+
api_name="connect_odoo"
|
226 |
+
)
|
227 |
+
|
228 |
+
def handle_get_model_fields_gui(selected_model):
|
229 |
+
"""
|
230 |
+
GUI handler: Retrieves the fields and their metadata for the selected Odoo model using credentials from environment variables.
|
231 |
+
Uses dropdown for model selection.
|
232 |
+
"""
|
233 |
+
url = os.environ.get("ODOO_URL")
|
234 |
+
db = os.environ.get("ODOO_DB")
|
235 |
+
username = os.environ.get("ODOO_USERNAME")
|
236 |
+
password = os.environ.get("ODOO_PASSWORD")
|
237 |
+
uid = authenticate(url, db, username, password)
|
238 |
+
if not all([selected_model, uid, url, db, password]):
|
239 |
+
return {"error": "Connection/model state missing. Reconnect."}
|
240 |
+
return get_model_fields(url, db, uid, password, selected_model)
|
241 |
+
|
242 |
+
def handle_get_model_fields_api(selected_model):
|
243 |
+
"""
|
244 |
+
MCP/API handler: Accepts any string for selected_model (from textbox, not dropdown).
|
245 |
+
"""
|
246 |
+
url = os.environ.get("ODOO_URL")
|
247 |
+
db = os.environ.get("ODOO_DB")
|
248 |
+
username = os.environ.get("ODOO_USERNAME")
|
249 |
+
password = os.environ.get("ODOO_PASSWORD")
|
250 |
+
uid = authenticate(url, db, username, password)
|
251 |
+
if not all([selected_model, uid, url, db, password]):
|
252 |
+
return {"error": "Connection/model state missing. Reconnect."}
|
253 |
+
return get_model_fields(url, db, uid, password, selected_model)
|
254 |
+
|
255 |
+
# GUI binding (dropdown)
|
256 |
+
get_fields_button.click(
|
257 |
+
fn=handle_get_model_fields_gui,
|
258 |
+
inputs=[model_dropdown],
|
259 |
+
outputs=[model_fields_output],
|
260 |
+
)
|
261 |
+
# MCP/API binding (textbox, hidden in GUI)
|
262 |
+
get_fields_api_button.click(
|
263 |
+
fn=handle_get_model_fields_api,
|
264 |
+
inputs=[model_name_api],
|
265 |
+
outputs=[model_fields_output],
|
266 |
+
api_name="get_model_fields"
|
267 |
+
)
|
268 |
+
|
269 |
+
def handle_search_read_click(selected_model, domain_str, limit):
|
270 |
+
"""
|
271 |
+
Searches for records in the selected model matching the provided domain and returns their details (search_read) using credentials from environment variables.
|
272 |
+
"""
|
273 |
+
url = os.environ.get("ODOO_URL")
|
274 |
+
db = os.environ.get("ODOO_DB")
|
275 |
+
username = os.environ.get("ODOO_USERNAME")
|
276 |
+
password = os.environ.get("ODOO_PASSWORD")
|
277 |
+
uid = authenticate(url, db, username, password)
|
278 |
+
if not all([selected_model, uid, url, db, password]):
|
279 |
+
return {"error": "Connection/model state missing."}
|
280 |
+
try:
|
281 |
+
domain = eval(domain_str) # Caution: eval can be risky
|
282 |
+
except Exception as e:
|
283 |
+
return {"error": f"Invalid domain: {e}"}
|
284 |
+
return search_read(url, db, uid, password, selected_model, domain, int(limit))
|
285 |
+
|
286 |
+
search_read_button.click(
|
287 |
+
fn=handle_search_read_click,
|
288 |
+
inputs=[model_dropdown, domain_input, limit_input],
|
289 |
+
outputs=[record_operation_output],
|
290 |
+
api_name="search_read"
|
291 |
+
)
|
292 |
+
|
293 |
+
def handle_search_click(selected_model: str, domain_str: str):
|
294 |
+
"""
|
295 |
+
Searches for record IDs in the selected Odoo model matching the provided domain using credentials from environment variables.
|
296 |
+
Args:
|
297 |
+
selected_model (str): The technical name of the Odoo model to search (e.g., 'res.partner').
|
298 |
+
domain_str (str): The search domain as a Python list-of-lists string (e.g., "[['is_company', '=', True]]").
|
299 |
+
Returns:
|
300 |
+
tuple: (result_dict, gradio_update)
|
301 |
+
- result_dict (dict): Contains either the number of record IDs found or an error message.
|
302 |
+
- gradio_update: Gradio Dropdown update object for record ID selection.
|
303 |
+
"""
|
304 |
+
url = os.environ.get("ODOO_URL")
|
305 |
+
db = os.environ.get("ODOO_DB")
|
306 |
+
username = os.environ.get("ODOO_USERNAME")
|
307 |
+
password = os.environ.get("ODOO_PASSWORD")
|
308 |
+
uid = authenticate(url, db, username, password)
|
309 |
+
if not all([selected_model, uid, url, db, password]):
|
310 |
+
return {"error": "Connection/model state missing."}, gr.update(choices=[])
|
311 |
+
try:
|
312 |
+
domain = eval(domain_str)
|
313 |
+
except Exception as e:
|
314 |
+
return {"error": f"Invalid domain: {e}"}, gr.update(choices=[])
|
315 |
+
record_ids = search(url, db, uid, password, selected_model, domain)
|
316 |
+
if isinstance(record_ids, dict) and 'error' in record_ids:
|
317 |
+
return record_ids, gr.update(choices=[])
|
318 |
+
if isinstance(record_ids, list):
|
319 |
+
id_choices = [(str(rid), rid) for rid in record_ids]
|
320 |
+
return {"record_ids_found": len(record_ids)}, gr.update(choices=id_choices, value=id_choices[0][1] if id_choices else None)
|
321 |
+
return {"error": "Unexpected search result"}, gr.update(choices=[])
|
322 |
+
|
323 |
+
search_button.click(
|
324 |
+
fn=handle_search_click,
|
325 |
+
inputs=[model_dropdown, domain_input],
|
326 |
+
outputs=[record_operation_output, record_id_dropdown],
|
327 |
+
api_name="search_ids"
|
328 |
+
)
|
329 |
+
|
330 |
+
def handle_read_click(selected_model: str, record_id: str):
|
331 |
+
"""
|
332 |
+
Reads and returns the details for a specific record ID from the selected Odoo model using credentials from environment variables.
|
333 |
+
Args:
|
334 |
+
selected_model (str): The technical name of the Odoo model to read from (e.g., 'res.partner').
|
335 |
+
record_id (str): The ID of the record to read (should be convertible to int).
|
336 |
+
Returns:
|
337 |
+
dict: Record details as returned by Odoo, or an error message dict.
|
338 |
+
"""
|
339 |
+
url = os.environ.get("ODOO_URL")
|
340 |
+
db = os.environ.get("ODOO_DB")
|
341 |
+
username = os.environ.get("ODOO_USERNAME")
|
342 |
+
password = os.environ.get("ODOO_PASSWORD")
|
343 |
+
uid = authenticate(url, db, username, password)
|
344 |
+
if not all([selected_model, record_id, uid, url, db, password]):
|
345 |
+
return {"error": "Connection/model/ID state missing."}
|
346 |
+
try:
|
347 |
+
rec_id = int(record_id)
|
348 |
+
except ValueError:
|
349 |
+
return {"error": f"Invalid Record ID: {record_id}. Must be int."}
|
350 |
+
return read(url, db, uid, password, selected_model, rec_id)
|
351 |
+
|
352 |
+
read_button.click(
|
353 |
+
fn=handle_read_click,
|
354 |
+
inputs=[model_dropdown, record_id_dropdown],
|
355 |
+
outputs=[record_operation_output],
|
356 |
+
api_name="read_record"
|
357 |
+
)
|
358 |
+
|
359 |
+
def handle_execute_kw_click(selected_model: str, method_name: str, args_str: str = "[]", kwargs_str: str = "{}"):
|
360 |
+
"""
|
361 |
+
Executes a generic method on the selected Odoo model using the provided method name, positional arguments, and keyword arguments.
|
362 |
+
Credentials are taken from environment variables.
|
363 |
+
Args:
|
364 |
+
selected_model (str): The technical name of the Odoo model (e.g., 'res.partner').
|
365 |
+
method_name (str): The technical name of the method to call (e.g., 'search', 'create').
|
366 |
+
args_str (str, optional): JSON string representing a list of positional arguments. Defaults to '[]'.
|
367 |
+
kwargs_str (str, optional): JSON string representing a dictionary of keyword arguments. Defaults to '{}'.
|
368 |
+
Returns:
|
369 |
+
dict: The result of the method call as returned by Odoo, or an error message dict.
|
370 |
+
"""
|
371 |
+
url = os.environ.get("ODOO_URL")
|
372 |
+
db = os.environ.get("ODOO_DB")
|
373 |
+
username = os.environ.get("ODOO_USERNAME")
|
374 |
+
password = os.environ.get("ODOO_PASSWORD")
|
375 |
+
uid = authenticate(url, db, username, password)
|
376 |
+
if not all([selected_model, method_name, uid, url, db, password]):
|
377 |
+
return {"error": "Connection/model/method state missing."}
|
378 |
+
try:
|
379 |
+
args = json.loads(args_str)
|
380 |
+
except Exception as e:
|
381 |
+
return {"error": f"Invalid JSON in Args: {e}"}
|
382 |
+
try:
|
383 |
+
kwargs = json.loads(kwargs_str)
|
384 |
+
except Exception as e:
|
385 |
+
return {"error": f"Invalid JSON in Kwargs: {e}"}
|
386 |
+
return execute_kw(url, db, uid, password, selected_model, method_name, args, kwargs)
|
387 |
+
|
388 |
+
execute_kw_button.click(
|
389 |
+
fn=handle_execute_kw_click,
|
390 |
+
inputs=[model_dropdown, method_name_input, args_input, kwargs_input],
|
391 |
+
outputs=[execute_kw_output],
|
392 |
+
api_name="execute_kw"
|
393 |
+
)
|
394 |
+
|
395 |
+
# --- New Order Creation Section ---
|
396 |
+
with gr.Group(visible=True) as order_creation_group:
|
397 |
+
gr.Markdown("## 5. Create New Order (Generic)")
|
398 |
+
gr.Markdown("""
|
399 |
+
Use this section to create a new order by specifying order lines and a note. Order lines should be provided as a JSON list of dicts, e.g.:
|
400 |
+
|
401 |
+
```json
|
402 |
+
[
|
403 |
+
{"product_id": 1, "product_uom_qty": 2, "price_unit": 10.0},
|
404 |
+
{"product_id": 2, "product_uom_qty": 1, "price_unit": 5.5}
|
405 |
+
]
|
406 |
+
```
|
407 |
+
Note is a free-form string. All fields are passed to Odoo's `sale.order` model via `create`.
|
408 |
+
""")
|
409 |
+
order_lines_input = gr.Textbox(label="Order Lines (JSON list of dicts)", placeholder='[{"product_id": 1, "product_uom_qty": 2, "price_unit": 10.0}]', lines=4)
|
410 |
+
note_input = gr.Textbox(label="Order Note", placeholder="Enter order note", lines=2)
|
411 |
+
create_order_button = gr.Button("Create Order")
|
412 |
+
create_order_output = gr.JSON(label="Order Creation Result")
|
413 |
+
|
414 |
+
def create_order_generic(order_lines_json: str, note: str):
|
415 |
+
"""
|
416 |
+
Create a new order in Odoo using the sale.order model and the create method, with credentials from environment variables.
|
417 |
+
Args:
|
418 |
+
order_lines_json (str): JSON string representing a list of order line dicts. Example:
|
419 |
+
'[{"product_id": 1, "product_uom_qty": 2, "price_unit": 10.0}]'
|
420 |
+
note (str): Optional note to attach to the order.
|
421 |
+
Returns:
|
422 |
+
dict: Contains the created order ID (as 'order_id') if successful, or an error message dict.
|
423 |
+
"""
|
424 |
+
url = os.environ.get("ODOO_URL")
|
425 |
+
db = os.environ.get("ODOO_DB")
|
426 |
+
username = os.environ.get("ODOO_USERNAME")
|
427 |
+
password = os.environ.get("ODOO_PASSWORD")
|
428 |
+
uid = authenticate(url, db, username, password)
|
429 |
+
try:
|
430 |
+
order_lines = json.loads(order_lines_json)
|
431 |
+
if not isinstance(order_lines, list):
|
432 |
+
return {"error": "Order lines must be a JSON list of dicts."}
|
433 |
+
except Exception as e:
|
434 |
+
return {"error": f"Invalid JSON in order lines: {e}"}
|
435 |
+
order_data = {
|
436 |
+
"order_line": [
|
437 |
+
(0, 0, line) for line in order_lines
|
438 |
+
],
|
439 |
+
"note": note or ""
|
440 |
+
}
|
441 |
+
result = execute_kw(url, db, uid, password, "sale.order", "create", [order_data], {"context": {"lang": "en_US"}})
|
442 |
+
return {"order_id": result} if isinstance(result, int) else result
|
443 |
+
|
444 |
+
create_order_button.click(
|
445 |
+
fn=create_order_generic,
|
446 |
+
inputs=[order_lines_input, note_input],
|
447 |
+
outputs=[create_order_output],
|
448 |
+
api_name="create_order"
|
449 |
+
)
|
450 |
+
|
451 |
+
if __name__ == "__main__":
|
452 |
+
demo.launch(mcp_server=True)
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=5.43.1
|
2 |
+
requests>=2.32.5
|
3 |
+
pydantic>=2.11.1
|
4 |
+
python-dotenv>=1.1.1
|