Merge pull request #15 from expsa/migration-module

migration to odo v18
This commit is contained in:
ahmed-nouri051 2025-09-24 11:32:55 +02:00 committed by GitHub
commit 99c9ea68ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 801 additions and 0 deletions

2
chatgpt_bot/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from . import controllers
from . import models

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
{
'name': "OdooBot ChatGPT AI integration",
'summary': """
This module integrates the response from ChatGPT into Odoo's built-in chatbot, OdooBot.
""",
'description': """
This module allows users to leverage the advanced natural language processing capabilities
of ChatGPT within Odoo's user-friendly interface. By integrating ChatGPT's responses into OdooBot,
users can easily access the powerful language model's insights and capabilities without having to
navigate away from the Odoo platform. This integration can be used to enhance the functionality of
OdooBot, providing more accurate and detailed responses to user queries and improving overall user experience.
""",
'author': "FL1 sro",
'website': "https://fl1.cz",
"images": ["static/description/banner.png", "static/description/gif_chat.gif"],
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/18.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Odex25-base',
'version': '18.0.0.1',
'license': 'AGPL-3',
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'queue_job'],
'installable': True,
'application': True, # <-- Important if you want it to appear in "Apps"
'auto_install': False,
# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/res_config_settings.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
"external_dependencies": {
"python": ["openai"]
},
'price': 0.00,
'currency': 'EUR',
}

View File

@ -0,0 +1,2 @@
from . import controllers

View File

30
chatgpt_bot/demo/demo.xml Normal file
View File

@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="chatgpt_blog.chatgpt_blog">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="chatgpt_blog.chatgpt_blog">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="chatgpt_blog.chatgpt_blog">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="chatgpt_blog.chatgpt_blog">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="chatgpt_blog.chatgpt_blog">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import res_config_setting
from . import mail_bot
from . import res_users

View File

@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
from odoo import api, models, _
from odoo.exceptions import UserError, ValidationError
from bs4 import BeautifulSoup as BS
# Note: Use the modern OpenAI client
# pip install openai
try:
from openai import OpenAI
except Exception: # pragma: no cover
OpenAI = None
class ChatGptBot(models.AbstractModel):
_inherit = 'mail.bot'
_description = 'ChatGPT OdooBot'
#################################################################################################
# ORM FUNCTIONS #
#################################################################################################
def create_content(self):
"""
Generates HTML content and SEO JSON via OpenAI and writes back to fields if present.
Expected optional fields on the record:
- text_chatgpt (input prompt)
- content (HTML output)
- website_meta_title / website_meta_description / website_meta_keywords (SEO)
- is_publish_now / is_published / is_elaborate
This method is defensive: if fields do not exist on the record, it skips setting them.
"""
# Ensure running on a single record or abstractly use 'self' as context holder
record = self
# Validate input field existence
text = getattr(record, 'text_chatgpt', None)
if not text:
raise UserError(_("Please provide a prompt (text_chatgpt) before generating content."))
api_key = self.env['ir.config_parameter'].sudo().get_param('chatgpt_api_key')
if not api_key:
raise UserError(_("No OpenAI API key configured. Set it in Settings → ChatGPT OdooBot."))
if OpenAI is None:
raise UserError(_("The OpenAI Python package is not installed. Please install 'openai'."))
# Define an example JSON schema for SEO guidance
ex_json = {
"title": "Sample Title",
"description": "Short description (max 160 characters)",
"keywords": "keyword1, keyword2, keyword3"
}
client = OpenAI(api_key=api_key)
# Prompt instructs the model to output: 1) an HTML <div> ... </div>, then 2) a pure JSON block
system_msg = "You are a helpful assistant that writes clean HTML and valid JSON."
user_msg = (
f"{text}\n\n"
"Output requirements:\n"
"1) First, return an HTML block wrapped strictly between <div> and </div> with headings, paragraphs, and links.\n"
"2) Immediately after the closing </div>, return ONLY a valid JSON object (no backticks, no labels)\n"
f"matching this schema and rules (description max 160 chars): {ex_json}\n"
)
try:
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_msg},
{"role": "user", "content": user_msg},
],
max_tokens=1200,
temperature=0.7,
)
full = resp.choices[0].message.content or ""
except Exception as e:
raise UserError(_("OpenAI request failed: %s") % str(e))
# Split HTML and JSON by the first </div>
html_part, json_part = "", ""
if "</div>" in full:
html_part = full.split("</div>", 1)[0] + "</div>"
json_part = full.split("</div>", 1)[1].strip()
# Parse SEO JSON if present
title = descr = kw = None
if json_part:
import json
try:
seo = json.loads(json_part)
title = seo.get("title")
descr = seo.get("description")
kw = seo.get("keywords")
except Exception:
# We don't hard-fail if JSON is malformed; raise a friendly error
raise ValidationError(_("The SEO JSON response is not valid JSON."))
# Assign fields defensively if they exist on the record
def safe_set(rec, field, value):
if hasattr(rec, field):
try:
setattr(rec, field, value)
except Exception:
pass
safe_set(record, 'content', html_part or "")
if title:
safe_set(record, 'website_meta_title', title)
if descr:
safe_set(record, 'website_meta_description', descr)
if kw:
safe_set(record, 'website_meta_keywords', kw)
# Handle publish flags if the model uses them
is_publish_now = getattr(record, 'is_publish_now', False)
if is_publish_now and hasattr(record, 'is_published'):
safe_set(record, 'is_published', True)
elif hasattr(record, 'is_published'):
safe_set(record, 'is_published', False)
safe_set(record, 'is_elaborate', True)
return True
#################################################################################################
# CUSTOM FUNCTIONS #
#################################################################################################
def _get_answer(self, record, body, values, command=None):
"""
Odoo 18-compatible signature. Adds #enable / #disable and routes to ChatGPT if enabled.
"""
res = super()._get_answer(record, body, values, command=command)
# Simple toggles
if body.strip().lower() == "#enable":
self.env.user.odoobot_state = 'chatgpt'
return _("ChatGPT enabled")
if body.strip().lower() == "#disable":
self.env.user.odoobot_state = 'disabled'
return _("ChatGPT disabled")
# Build a short context from last messages (plaintext only)
channel = self.env['mail.channel'].browse(record.id)
last_ids = channel.message_ids.ids
messages = self.env['mail.message'].search([('id', 'in', last_ids)], order='id desc', limit=2).mapped('body')
old_conv = ""
for msg in messages:
if msg:
old_conv += BS(msg, 'html.parser').get_text() + "\n"
# Route to ChatGPT if enabled
if self.env.user.odoobot_state == 'chatgpt':
return self._chatgpt_reply(record, body, old_conv)
return res
def _chatgpt_reply(self, record, body, context_text=""):
api_key = self.env['ir.config_parameter'].sudo().get_param('chatgpt_api_key')
if not api_key:
raise UserError(_("No OpenAI API key configured. Set it in Settings → ChatGPT OdooBot."))
if OpenAI is None:
raise UserError(_("The OpenAI Python package is not installed. Please install 'openai'."))
client = OpenAI(api_key=api_key)
try:
messages = []
if context_text:
messages.append({"role": "system", "content": f"Conversation context:\n{context_text.strip()}"})
messages.append({"role": "user", "content": body})
resp = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=messages,
max_tokens=800,
temperature=0.7,
)
reply = resp.choices[0].message.content or ""
except Exception as e:
raise UserError(_("OpenAI request failed: %s") % str(e))
gpt_html = "<strong>OpenAI:</strong> " + reply
author_id = self.env.ref("base.partner_root").id
subtype_id = self.env.ref("mail.mt_comment").id
self.env['mail.channel'].browse(record.id).message_post(
body=gpt_html,
message_type='comment',
subtype_id=subtype_id,
author_id=author_id,
)
return gpt_html

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
apikey = fields.Char(
string="API Key",
config_parameter="chatgpt_blog.apikey"
)

View File

@ -0,0 +1,10 @@
from odoo import api, fields, models, _
class ResUsers(models.Model):
_inherit = 'res.users'
odoobot_state = fields.Selection(
selection_add=[('chatgpt', 'ChatGPT')],
)

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_chatgpt_blog_chatgpt_blog,chatgpt_blog.chatgpt_blog,model_chatgpt_blog_chatgpt_blog,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_chatgpt_blog_chatgpt_blog chatgpt_blog.chatgpt_blog model_chatgpt_blog_chatgpt_blog base.group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,449 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<div class="section" id="openai-chatgpt-odoo-module">
<h1>OpenAI ChatGPT Odoo Module</h1>
<p><a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a></p>
<dl class="docutils">
<dt>This Odoo module allows for seamless integration with the OpenAI ChatGPT API,</dt>
<dd>providing advanced natural language processing capabilities within the Odoo platform.
With this module, users can leverage the power of ChatGPT to generate human-like text,
perform language translation, and more.
It utilizes API calls to communicate with the OpenAI service,
making it easy to set up and use.</dd>
</dl>
<p><strong>Table of contents</strong></p>
</div>
<div class="section" id="configuration">
<h1>Configuration</h1>
<ul class="simple">
<li>Go to 'Settings / chatGPT bot and set an API key'.</li>
<li>you can retrive an api Key from <a class="reference external" href="https://beta.openai.com/">https://beta.openai.com/</a> and create a new key here: <a class="reference external" href="https://beta.openai.com/account/api-keys">https://beta.openai.com/account/api-keys</a></li>
<li>This module require the offical library openai</li>
<li>you install with <tt class="docutils literal">pip install openai</tt></li>
</ul>
</div>
<div class="section" id="bugfix">
<h1>Bugfix</h1>
<ul class="simple">
<li>fix error when no api key is set</li>
</ul>
</div>
<div class="section" id="usage">
<h1>Usage</h1>
<p>To use this module, you need to:</p>
<ul class="simple">
<li>require queque_job from <a class="reference external" href="https://github.com/OCA/queue/tree/14.0/queue_job">queue_job</a>.</li>
<li>After the installation to this module, you must change the configuration and add this on the configuration file:</li>
<li><tt class="docutils literal">server_wide_modules = base,web,queue_job</tt> add <tt class="docutils literal">queue_job</tt></li>
<li>Then restart the server and see a logs file in server console.</li>
</ul>
<img alt="images/image1.png" src="images/image1.png" />
<p><strong>This module is required for not blocking the server when the API is called.</strong></p>
</div>
<div class="section" id="configuration-users">
<h1>Configuration users</h1>
<p>inside the preferences of the user you can set the chatGPT bot as default bot.</p>
<img alt="images/image2.png" src="images/image2.png" />
<p><strong>You can activate or deactivate the chatGPT bot for each user.</strong>
<strong>You can activate or deactivate the chatGPT directly in the chat window.</strong></p>
<p>You can type: <tt class="docutils literal">#enable</tt> or <tt class="docutils literal">#disable</tt></p>
<img alt="images/image3.png" src="images/image3.png" />
</div>
<div class="section" id="example">
<h1>Example</h1>
<img alt="images/image4.png" src="images/image4.png" />
<img alt="images/gif_chat.gif" src="images/gif_chat.gif" />
</div>
<div class="section" id="bug-tracker">
<h1>Bug Tracker</h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/crottolo/free_addons/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1>Credits</h1>
<div class="section" id="authors">
<h2>Authors</h2>
<ul class="simple">
<li>FL1 sro</li>
</ul>
</div>
<div class="section" id="contributors">
<h2>Contributors</h2>
<ul class="simple">
<li>Roberto Crotti &lt;<a class="reference external" href="mailto:bo&#64;fl1.cz">bo&#64;fl1.cz</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2>Maintainers</h2>
<p>This module is maintained by the FL1.</p>
<a class="reference external image-reference" href="https://fl1.cz"><img alt="Odoo Fl1 sro" src="https://fl1.cz/web/image/1156-2d6fce00/FL1%20logo%20def.png" style="width: 50px;" /></a>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form_inherit_chatgpt" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.chatgpt</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<!-- Insert inside the main form -->
<xpath expr="//form" position="inside">
<div class="app_settings_block" string="ChatGPT BOT"
data-key="chatgpt_blog" data-string="ChatGPT Blog">
<h2>ChatGPT OdooBot</h2>
<div class="row mt16 o_settings_container" id="chatgpt_bot_setting">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"></div>
<div class="o_setting_right_pane">
<div class="content-group">
<div class="row">
<label for="apikey" string="API Key"
class="col-md-3 o_light_label"/>
<field name="apikey"/>
</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
<record id="chatgpt_blog_config_settings_action" model="ir.actions.act_window">
<field name="name">ChatGPT Settings</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">current</field>
<field name="context">{'module': 'chatgpt_blog'}</field>
</record>
</data>
</odoo>

1
queue Submodule

@ -0,0 +1 @@
Subproject commit edc21e4c4ef11a1ef746ca5ac641e9227602a35d