ObiJuanCodenobi commited on
Commit
82e652d
·
1 Parent(s): 8e52d5a
Files changed (2) hide show
  1. app.py +449 -4
  2. requirements.txt +4 -0
app.py CHANGED
@@ -1,7 +1,452 @@
 
1
  import gradio as gr
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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