Refactor: Move SEO content files to root of module
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Brand Voice List View -->
|
||||
<record id="otk_seo_brand_voice_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.brand.voice.list</field>
|
||||
<field name="model">otk.seo.brand.voice</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Brand Voices">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="voice_tone"/>
|
||||
<field name="writing_style"/>
|
||||
<field name="is_default" widget="boolean_toggle"/>
|
||||
<field name="content_count"/>
|
||||
<field name="active" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Brand Voice Form View -->
|
||||
<record id="otk_seo_brand_voice_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.brand.voice.form</field>
|
||||
<field name="model">otk.seo.brand.voice</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Brand Voice">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_content" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text-o">
|
||||
<field name="content_count" widget="statinfo" string="Content"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<widget name="web_ribbon" title="Default"
|
||||
bg_color="text-bg-success" invisible="not is_default"/>
|
||||
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Brand Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Settings">
|
||||
<field name="is_default" widget="boolean_toggle"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group string="Voice Style">
|
||||
<field name="voice_tone"/>
|
||||
<field name="writing_style"/>
|
||||
<field name="voice_personality"
|
||||
placeholder="e.g., innovative, trustworthy, approachable"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Brand Identity" name="identity">
|
||||
<group>
|
||||
<group string="Brand Description">
|
||||
<field name="brand_description" nolabel="1" colspan="2"
|
||||
placeholder="Describe your brand, its values, and mission..."/>
|
||||
</group>
|
||||
<group string="Target Audience">
|
||||
<field name="target_audience" nolabel="1" colspan="2"
|
||||
placeholder="Describe your ideal reader/customer..."/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Vocabulary" name="vocabulary">
|
||||
<group>
|
||||
<group string="Preferred Words">
|
||||
<field name="preferred_words" nolabel="1" colspan="2"
|
||||
placeholder="Words to use (one per line)..."/>
|
||||
</group>
|
||||
<group string="Words to Avoid">
|
||||
<field name="avoided_words" nolabel="1" colspan="2"
|
||||
placeholder="Words to avoid (one per line)..."/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Industry Terminology">
|
||||
<field name="industry_terms" nolabel="1" colspan="2"
|
||||
placeholder="Industry terms and preferred usage..."/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Custom Instructions" name="instructions">
|
||||
<group string="Additional AI Instructions">
|
||||
<field name="custom_instructions" nolabel="1" colspan="2"
|
||||
placeholder="Additional rules for the AI to follow when generating content..."/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Examples" name="examples">
|
||||
<group>
|
||||
<group string="Good Example">
|
||||
<field name="example_good" nolabel="1" colspan="2"
|
||||
placeholder="Paste content that represents your brand voice well..."/>
|
||||
</group>
|
||||
<group string="What to Avoid">
|
||||
<field name="example_bad" nolabel="1" colspan="2"
|
||||
placeholder="Paste content that does NOT match your brand voice..."/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Brand Voice Search View -->
|
||||
<record id="otk_seo_brand_voice_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.brand.voice.search</field>
|
||||
<field name="model">otk.seo.brand.voice</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Brand Voices">
|
||||
<field name="name"/>
|
||||
<field name="voice_tone"/>
|
||||
<separator/>
|
||||
<filter name="filter_default" string="Default Voice"
|
||||
domain="[('is_default', '=', True)]"/>
|
||||
<filter name="filter_active" string="Active"
|
||||
domain="[('active', '=', True)]"/>
|
||||
<filter name="filter_archived" string="Archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_otk_seo_brand_voice" model="ir.actions.act_window">
|
||||
<field name="name">Brand Voices</field>
|
||||
<field name="res_model">otk.seo.brand.voice</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="otk_seo_brand_voice_view_search"/>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first brand voice!
|
||||
</p>
|
||||
<p>
|
||||
Define your brand's tone, style, and vocabulary to ensure
|
||||
all AI-generated content follows your brand guidelines.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<menuitem id="menu_seo_content_root"
|
||||
name="SEO Content"
|
||||
groups="otoolkit_seo_content.group_seo_content_user"
|
||||
web_icon="otoolkit_seo_content,static/description/icon.png"
|
||||
sequence="90"/>
|
||||
|
||||
<!-- Content Management Submenu -->
|
||||
<menuitem id="menu_seo_content_generator"
|
||||
name="Content"
|
||||
parent="menu_seo_content_root"
|
||||
sequence="5"/>
|
||||
|
||||
<!-- Quick Action: Generate Content (prominent at top) -->
|
||||
<menuitem id="menu_generate_content"
|
||||
name="Generate Content"
|
||||
parent="menu_seo_content_root"
|
||||
action="action_generate_content_wizard"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_my_content"
|
||||
name="All Content"
|
||||
parent="menu_seo_content_generator"
|
||||
action="action_otk_seo_content"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_bulk_generation"
|
||||
name="Bulk Jobs"
|
||||
parent="menu_seo_content_generator"
|
||||
action="action_otk_seo_content_batch"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_ideas_queue"
|
||||
name="Ideas Queue"
|
||||
parent="menu_seo_content_generator"
|
||||
action="action_otk_seo_content_idea"
|
||||
sequence="25"/>
|
||||
|
||||
<menuitem id="menu_generated_images"
|
||||
name="Images"
|
||||
parent="menu_seo_content_generator"
|
||||
action="action_otk_seo_content_image"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Reporting Submenu -->
|
||||
<menuitem id="menu_seo_content_reporting"
|
||||
name="Reporting"
|
||||
parent="menu_seo_content_root"
|
||||
sequence="80"/>
|
||||
|
||||
<menuitem id="menu_seo_dashboard"
|
||||
name="Dashboard"
|
||||
parent="menu_seo_content_reporting"
|
||||
action="action_seo_content_dashboard_owl"
|
||||
sequence="5"/>
|
||||
|
||||
<menuitem id="menu_token_usage"
|
||||
name="Token Usage"
|
||||
parent="menu_seo_content_reporting"
|
||||
action="action_seo_content_token_usage"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_content_by_type"
|
||||
name="Content by Type"
|
||||
parent="menu_seo_content_reporting"
|
||||
action="action_seo_content_by_type"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_content_by_state"
|
||||
name="Content by Status"
|
||||
parent="menu_seo_content_reporting"
|
||||
action="action_seo_content_by_state"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Configuration Submenu -->
|
||||
<menuitem id="menu_seo_content_config"
|
||||
name="Configuration"
|
||||
parent="menu_seo_content_root"
|
||||
sequence="90"
|
||||
groups="base.group_system"/>
|
||||
|
||||
<menuitem id="menu_content_templates"
|
||||
name="Content Templates"
|
||||
parent="menu_seo_content_config"
|
||||
action="action_otk_seo_template"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_brand_voices"
|
||||
name="Brand Voices"
|
||||
parent="menu_seo_content_config"
|
||||
action="action_otk_seo_brand_voice"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_reference_library"
|
||||
name="Reference Library"
|
||||
parent="menu_seo_content_config"
|
||||
action="action_otk_seo_reference"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_reference_tags"
|
||||
name="Reference Tags"
|
||||
parent="menu_seo_content_config"
|
||||
action="action_otk_seo_reference_tag"
|
||||
sequence="35"/>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form_seo_content" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.seo.content</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="otoolkit_auth.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//app[@name='otoolkit_auth']" position="after">
|
||||
<app string="SEO Content" name="otoolkit_seo_content">
|
||||
<block title="SEO Content Settings">
|
||||
<setting string="Default Blog" help="Default blog for publishing generated content">
|
||||
<field name="seo_default_blog_id"/>
|
||||
</setting>
|
||||
<setting string="Default Image Count" help="Number of images to generate by default">
|
||||
<field name="seo_default_image_count"/>
|
||||
</setting>
|
||||
<setting string="Default Image Quality">
|
||||
<field name="seo_default_image_quality" widget="radio"/>
|
||||
</setting>
|
||||
<setting string="Auto-generate Alt Text" help="Automatically create SEO-friendly alt text for images">
|
||||
<field name="seo_auto_generate_alt_text"/>
|
||||
</setting>
|
||||
<setting string="Version History Limit" help="Maximum number of content versions to keep">
|
||||
<field name="seo_max_versions"/>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,245 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Batch List View -->
|
||||
<record id="otk_seo_content_batch_view_list" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.batch.list</field>
|
||||
<field name="model">otk.seo.content.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Batch Jobs" decoration-info="state == 'draft'"
|
||||
decoration-warning="state == 'processing'"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'failed'">
|
||||
<field name="name"/>
|
||||
<field name="source_type"/>
|
||||
<field name="content_type"/>
|
||||
<field name="total_items"/>
|
||||
<field name="completed_items"/>
|
||||
<field name="failed_items"/>
|
||||
<field name="progress_percent" widget="progressbar"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state in ('queued', 'processing')"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'failed'"/>
|
||||
<field name="create_date" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Batch Form View -->
|
||||
<record id="otk_seo_content_batch_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.batch.form</field>
|
||||
<field name="model">otk.seo.content.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Batch Content Generation">
|
||||
<header>
|
||||
<button name="action_prepare_items" string="Add Items from Source"
|
||||
type="object" class="btn-secondary" icon="fa-plus"
|
||||
invisible="state != 'draft' or not source_type"
|
||||
help="Add items from the selected source (products, CSV, or keywords). Existing items are preserved."/>
|
||||
<button name="action_clear_items" string="Clear All Items"
|
||||
type="object" class="btn-secondary" icon="fa-trash"
|
||||
invisible="state != 'draft' or total_items == 0"
|
||||
confirm="This will remove all items from the batch. Continue?"
|
||||
help="Remove all items from the batch to start fresh."/>
|
||||
<button name="action_start_batch" string="Start Generation"
|
||||
type="object" class="btn-primary"
|
||||
invisible="state != 'draft' or total_items == 0"
|
||||
confirm="This will start generating content for all items. Continue?"
|
||||
help="Start generating content for all prepared items. Items will be processed one by one."/>
|
||||
<button name="action_pause" string="Pause"
|
||||
type="object" class="btn-secondary" icon="fa-pause"
|
||||
invisible="state not in ('queued', 'processing')"
|
||||
help="Pause batch processing. Items currently in progress will finish."/>
|
||||
<button name="action_resume" string="Resume"
|
||||
type="object" class="btn-primary" icon="fa-play"
|
||||
invisible="state != 'paused'"
|
||||
help="Resume paused batch processing."/>
|
||||
<button name="action_cancel" string="Cancel"
|
||||
type="object" class="btn-secondary"
|
||||
invisible="state not in ('queued', 'processing', 'paused')"
|
||||
help="Stop the batch processing. Items already completed will be kept."/>
|
||||
<button name="action_retry_failed" string="Retry Failed"
|
||||
type="object" class="btn-warning"
|
||||
invisible="failed_items == 0 or state not in ('done', 'failed')"
|
||||
help="Retry generating content for all failed items"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,queued,processing,done"
|
||||
statusbar_colors='{"paused": "warning"}'/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_content" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text-o"
|
||||
invisible="content_count == 0">
|
||||
<field name="content_count" widget="statinfo" string="Content"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Batch Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Paused Alert -->
|
||||
<div class="alert alert-warning" role="alert"
|
||||
invisible="state != 'paused'">
|
||||
<strong><i class="fa fa-pause-circle"/> Paused:</strong>
|
||||
Batch processing is paused. Click "Resume" to continue.
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar with ETA -->
|
||||
<div class="alert alert-info" role="alert"
|
||||
invisible="state not in ('queued', 'processing')">
|
||||
<strong>Processing:</strong>
|
||||
<field name="completed_items" class="oe_inline"/> /
|
||||
<field name="total_items" class="oe_inline"/> items completed
|
||||
<span invisible="eta_minutes == 0" class="ms-2">
|
||||
— ETA: <field name="eta_minutes" class="oe_inline"/> min
|
||||
</span>
|
||||
<field name="progress_percent" widget="progressbar" class="mt-2"/>
|
||||
</div>
|
||||
|
||||
<!-- Error Summary -->
|
||||
<div class="alert alert-warning" role="alert" invisible="failed_items == 0">
|
||||
<strong>Warning:</strong>
|
||||
<field name="failed_items" class="oe_inline"/> items failed.
|
||||
Check the items below for error details.
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Source">
|
||||
<field name="concurrency" readonly="state != 'draft'"
|
||||
help="Items to process per cron run (1-10)"/>
|
||||
<field name="source_type" widget="radio"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="product_ids" widget="many2many_tags"
|
||||
invisible="source_type != 'products'"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="csv_file" filename="csv_filename"
|
||||
invisible="source_type != 'csv'"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="csv_filename" invisible="1"/>
|
||||
<field name="keywords_list"
|
||||
invisible="source_type != 'keywords'"
|
||||
readonly="state != 'draft'"
|
||||
placeholder="keyword1, topic1 keyword2, topic2 ..."/>
|
||||
</group>
|
||||
<group string="Content Settings">
|
||||
<field name="content_type" readonly="state != 'draft'"/>
|
||||
<field name="template_id" readonly="state != 'draft'"
|
||||
domain="[('content_type', '=', content_type)]"/>
|
||||
<field name="brand_voice_id" readonly="state != 'draft'"/>
|
||||
<field name="tone" readonly="state != 'draft'"/>
|
||||
<field name="target_word_count" readonly="state != 'draft'"/>
|
||||
<field name="language_id" readonly="state != 'draft'"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<group string="Image Settings">
|
||||
<field name="include_images" widget="boolean_toggle"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="image_count" invisible="not include_images"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="image_style" invisible="not include_images"
|
||||
readonly="state != 'draft'"/>
|
||||
</group>
|
||||
<group string="Blog Settings" invisible="content_type != 'blog_post'">
|
||||
<field name="blog_id" readonly="state != 'draft'"
|
||||
placeholder="Select target blog"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group invisible="state == 'draft'">
|
||||
<group string="Timing">
|
||||
<field name="started_at"/>
|
||||
<field name="completed_at"/>
|
||||
<field name="duration" widget="float_time"/>
|
||||
</group>
|
||||
<group/>
|
||||
</group>
|
||||
|
||||
<notebook invisible="not item_ids">
|
||||
<page string="Items" name="items">
|
||||
<field name="item_ids" readonly="state != 'draft'">
|
||||
<list decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'failed'"
|
||||
decoration-warning="state == 'processing'"
|
||||
decoration-muted="state == 'cancelled'"
|
||||
editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="keywords"/>
|
||||
<field name="topic" optional="hide"/>
|
||||
<field name="product_id" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state in ('draft', 'pending')"
|
||||
decoration-warning="state == 'processing'"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'failed'"
|
||||
readonly="1"/>
|
||||
<field name="content_id" optional="show" readonly="1"/>
|
||||
<field name="error_message" optional="hide" readonly="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Batch Search View -->
|
||||
<record id="otk_seo_content_batch_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.batch.search</field>
|
||||
<field name="model">otk.seo.content.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Batches">
|
||||
<field name="name"/>
|
||||
<separator/>
|
||||
<filter name="filter_draft" string="Draft"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="filter_processing" string="Processing"
|
||||
domain="[('state', 'in', ('queued', 'processing'))]"/>
|
||||
<filter name="filter_done" string="Completed"
|
||||
domain="[('state', '=', 'done')]"/>
|
||||
<filter name="filter_failed" string="Failed"
|
||||
domain="[('state', '=', 'failed')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_my_batches" string="My Batches"
|
||||
domain="[('create_uid', '=', uid)]"/>
|
||||
<group>
|
||||
<filter name="group_state" string="Status"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter name="group_source" string="Source Type"
|
||||
context="{'group_by': 'source_type'}"/>
|
||||
<filter name="group_date" string="Created Date"
|
||||
context="{'group_by': 'create_date:month'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Batch Action -->
|
||||
<record id="action_otk_seo_content_batch" model="ir.actions.act_window">
|
||||
<field name="name">Bulk Generation</field>
|
||||
<field name="res_model">otk.seo.content.batch</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="otk_seo_content_batch_view_search"/>
|
||||
<field name="context">{'search_default_filter_my_batches': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first bulk generation job!
|
||||
</p>
|
||||
<p>
|
||||
Generate SEO content for multiple products or topics at once.
|
||||
Select products, upload a CSV, or enter keywords to get started.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,332 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- SEO Content Idea List View -->
|
||||
<record id="otk_seo_content_idea_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.idea.list</field>
|
||||
<field name="model">otk.seo.content.idea</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Content Ideas"
|
||||
decoration-info="state == 'idea'"
|
||||
decoration-warning="state == 'scheduled'"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'failed'"
|
||||
decoration-muted="state == 'cancelled'">
|
||||
<field name="priority" widget="priority"/>
|
||||
<field name="name"/>
|
||||
<field name="content_type"/>
|
||||
<field name="keywords" optional="show"/>
|
||||
<field name="is_recurring" string="Rec." widget="boolean" optional="show"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'idea'"
|
||||
decoration-warning="state in ('scheduled', 'generating')"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'failed'"
|
||||
decoration-muted="state == 'cancelled'"/>
|
||||
<field name="content_id" optional="show"/>
|
||||
<button name="action_generate_now" type="object"
|
||||
string="Generate Now" icon="fa-bolt"
|
||||
invisible="state not in ('idea', 'scheduled')"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Idea Form View -->
|
||||
<record id="otk_seo_content_idea_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.idea.form</field>
|
||||
<field name="model">otk.seo.content.idea</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Content Idea">
|
||||
<header>
|
||||
<button name="action_schedule_now" type="object"
|
||||
string="Schedule Now" class="oe_highlight"
|
||||
invisible="state != 'idea'"/>
|
||||
<button name="action_generate_now" type="object"
|
||||
string="Generate Now" class="oe_highlight"
|
||||
invisible="state not in ('idea', 'scheduled')"/>
|
||||
<button name="action_cancel" type="object"
|
||||
string="Cancel"
|
||||
invisible="state not in ('idea', 'scheduled')"/>
|
||||
<button name="action_reset_to_idea" type="object"
|
||||
string="Reset to Idea"
|
||||
invisible="state not in ('failed', 'cancelled')"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="idea,scheduled,generating,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_content" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text-o"
|
||||
invisible="not content_id">
|
||||
<span class="o_stat_text">View Content</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<widget name="web_ribbon" title="Failed"
|
||||
bg_color="text-bg-danger" invisible="state != 'failed'"/>
|
||||
<widget name="web_ribbon" title="Cancelled"
|
||||
bg_color="text-bg-secondary" invisible="state != 'cancelled'"/>
|
||||
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Content idea title or topic..."
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Idea Details">
|
||||
<field name="priority" widget="priority"/>
|
||||
<field name="keywords" placeholder="target, keywords, here"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="topic_brief" placeholder="Describe what the content should cover..."
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="product_id"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="reference_ids" widget="many2many_tags"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</group>
|
||||
<group string="Schedule">
|
||||
<field name="scheduled_date"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="generated_at" readonly="1"
|
||||
invisible="state != 'done'"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Generation Settings" name="settings">
|
||||
<group>
|
||||
<group>
|
||||
<field name="content_type"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="template_id"
|
||||
domain="[('content_type', '=', content_type)]"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="brand_voice_id"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="tone"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="target_word_count"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="language_id"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Images" name="images">
|
||||
<group>
|
||||
<group>
|
||||
<field name="include_images"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="image_count"
|
||||
invisible="not include_images"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="image_style"
|
||||
invisible="not include_images"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Recurring" name="recurring">
|
||||
<group>
|
||||
<group>
|
||||
<field name="is_recurring"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="recurrence_type"
|
||||
invisible="not is_recurring"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="recurrence_end_date"
|
||||
invisible="not is_recurring"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="parent_idea_id" readonly="1"
|
||||
invisible="not parent_idea_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert"
|
||||
invisible="not is_recurring">
|
||||
<i class="fa fa-repeat" title="Recurring"/>
|
||||
<strong> Recurring Idea:</strong>
|
||||
A new idea will automatically be created and scheduled
|
||||
after this one is processed.
|
||||
</div>
|
||||
</page>
|
||||
|
||||
<page string="Auto-Publish" name="publish">
|
||||
<group>
|
||||
<group>
|
||||
<field name="auto_create_blog_post"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="blog_id"
|
||||
invisible="not auto_create_blog_post"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
<field name="auto_publish"
|
||||
invisible="not auto_create_blog_post"
|
||||
readonly="state not in ('idea', 'scheduled')"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-warning" role="alert"
|
||||
invisible="not auto_publish">
|
||||
<i class="fa fa-exclamation-triangle" title="Auto-Publish"/>
|
||||
<strong>Auto-Publish Enabled:</strong>
|
||||
Content will be automatically published to your website when generated.
|
||||
Make sure you review scheduled ideas before their scheduled time.
|
||||
</div>
|
||||
</page>
|
||||
|
||||
<page string="Result" name="result" invisible="state not in ('done', 'failed')">
|
||||
<group>
|
||||
<group>
|
||||
<field name="content_id" readonly="1"/>
|
||||
<field name="blog_post_id" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="generated_at" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Error Details" invisible="state != 'failed'">
|
||||
<field name="error_message" readonly="1" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Idea Search View -->
|
||||
<record id="otk_seo_content_idea_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.idea.search</field>
|
||||
<field name="model">otk.seo.content.idea</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Ideas">
|
||||
<field name="name"/>
|
||||
<field name="keywords"/>
|
||||
<separator/>
|
||||
<filter name="filter_ideas" string="Ideas"
|
||||
domain="[('state', '=', 'idea')]"/>
|
||||
<filter name="filter_scheduled" string="Scheduled"
|
||||
domain="[('state', '=', 'scheduled')]"/>
|
||||
<filter name="filter_done" string="Completed"
|
||||
domain="[('state', '=', 'done')]"/>
|
||||
<filter name="filter_failed" string="Failed"
|
||||
domain="[('state', '=', 'failed')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_high_priority" string="High Priority"
|
||||
domain="[('priority', 'in', ('2', '3'))]"/>
|
||||
<filter name="filter_auto_publish" string="Auto-Publish"
|
||||
domain="[('auto_publish', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter name="filter_my_ideas" string="My Ideas"
|
||||
domain="[('create_uid', '=', uid)]"/>
|
||||
<group>
|
||||
<filter name="group_state" string="Status"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter name="group_type" string="Content Type"
|
||||
context="{'group_by': 'content_type'}"/>
|
||||
<filter name="group_priority" string="Priority"
|
||||
context="{'group_by': 'priority'}"/>
|
||||
<filter name="group_scheduled" string="Scheduled Date"
|
||||
context="{'group_by': 'scheduled_date:day'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Idea Kanban View -->
|
||||
<record id="otk_seo_content_idea_view_kanban" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.idea.kanban</field>
|
||||
<field name="model">otk.seo.content.idea</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_kanban_small_column">
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<field name="priority"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="content_type"/>
|
||||
<field name="auto_publish"/>
|
||||
<progressbar field="state"
|
||||
colors='{"idea": "info", "scheduled": "warning", "generating": "warning", "done": "success", "failed": "danger", "cancelled": "muted"}'/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_title d-flex align-items-center">
|
||||
<field name="priority" widget="priority" class="me-2"/>
|
||||
<strong class="text-truncate"><field name="name"/></strong>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<field name="content_type"/>
|
||||
<t t-if="record.scheduled_date.raw_value">
|
||||
<br/>
|
||||
<span class="text-muted">
|
||||
<i class="fa fa-clock-o" title="Scheduled Date"/> <field name="scheduled_date"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom mt-2">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<t t-if="record.auto_publish.raw_value">
|
||||
<span class="badge text-bg-warning" title="Auto-Publish">
|
||||
<i class="fa fa-globe"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="state" widget="label_selection"
|
||||
options="{'classes': {'idea': 'info', 'scheduled': 'warning', 'generating': 'warning', 'done': 'success', 'failed': 'danger', 'cancelled': 'secondary'}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Idea Calendar View -->
|
||||
<record id="otk_seo_content_idea_view_calendar" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.idea.calendar</field>
|
||||
<field name="model">otk.seo.content.idea</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="Scheduled Ideas" date_start="scheduled_date"
|
||||
color="priority" mode="month" quick_create="0"
|
||||
event_open_popup="true">
|
||||
<field name="name"/>
|
||||
<field name="content_type"/>
|
||||
<field name="keywords"/>
|
||||
<field name="state"/>
|
||||
<field name="auto_publish"/>
|
||||
<field name="is_recurring"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_otk_seo_content_idea" model="ir.actions.act_window">
|
||||
<field name="name">Ideas Queue</field>
|
||||
<field name="res_model">otk.seo.content.idea</field>
|
||||
<field name="view_mode">kanban,list,form,calendar</field>
|
||||
<field name="search_view_id" ref="otk_seo_content_idea_view_search"/>
|
||||
<field name="context">{'search_default_filter_ideas': 1, 'search_default_filter_scheduled': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add your first content idea!
|
||||
</p>
|
||||
<p>
|
||||
Create content ideas and schedule them for automatic generation.
|
||||
Ideas can include keywords, topic briefs, and publishing settings.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,226 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- SEO Content Image List View -->
|
||||
<record id="otk_seo_content_image_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.image.list</field>
|
||||
<field name="model">otk.seo.content.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Generated Images"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state in ('pending', 'processing')"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'error'">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="image" widget="image" options="{'size': [64, 64]}"/>
|
||||
<field name="name"/>
|
||||
<field name="content_id"/>
|
||||
<field name="style"/>
|
||||
<field name="aspect_ratio"/>
|
||||
<field name="state" widget="badge"/>
|
||||
<field name="is_cover"/>
|
||||
<field name="is_optimized" widget="boolean"/>
|
||||
<field name="size_reduction" optional="hide"/>
|
||||
<field name="alt_text"/>
|
||||
<field name="token_cost" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Image Form View -->
|
||||
<record id="otk_seo_content_image_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.image.form</field>
|
||||
<field name="model">otk.seo.content.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Generated Image">
|
||||
<header>
|
||||
<button name="action_generate" type="object" string="Generate"
|
||||
class="oe_highlight" invisible="state not in ('draft', 'pending')"/>
|
||||
<button name="action_regenerate" type="object" string="Regenerate"
|
||||
class="oe_highlight" icon="fa-refresh"
|
||||
invisible="state not in ('done', 'error')"
|
||||
confirm="This will generate a new image with the current settings. The previous image will be saved as backup. Continue?"/>
|
||||
<button name="action_poll_status" type="object" string="Check Status"
|
||||
invisible="state != 'processing'"/>
|
||||
<button name="action_retry" type="object" string="Retry"
|
||||
invisible="state != 'error'"/>
|
||||
<button name="action_optimize" type="object" string="Optimize for Web"
|
||||
class="btn-secondary" invisible="state != 'done' or is_optimized"/>
|
||||
<button name="action_restore_original" type="object" string="Restore Original"
|
||||
class="btn-secondary" invisible="not image_original"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,pending,processing,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Error"
|
||||
bg_color="text-bg-danger" invisible="state != 'error'"/>
|
||||
<widget name="web_ribbon" title="Optimized"
|
||||
bg_color="text-bg-success" invisible="not is_optimized"/>
|
||||
|
||||
<group>
|
||||
<group string="Image">
|
||||
<field name="image" widget="image"
|
||||
options="{'size': [400, 400]}"
|
||||
invisible="state != 'done'"/>
|
||||
<field name="image_original" widget="image"
|
||||
options="{'size': [200, 200]}" readonly="1"
|
||||
invisible="not image_original"
|
||||
string="Previous Image (Backup)"/>
|
||||
<field name="name"/>
|
||||
<field name="content_id" readonly="state != 'draft'"/>
|
||||
</group>
|
||||
<group string="Generation Settings">
|
||||
<field name="style" readonly="state == 'processing'"/>
|
||||
<field name="aspect_ratio" readonly="state == 'processing'"/>
|
||||
<field name="quality" readonly="state == 'processing'"/>
|
||||
<field name="is_cover"/>
|
||||
<field name="is_og_image"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Generation Prompt">
|
||||
<field name="prompt" placeholder="Describe the image you want to generate..."
|
||||
readonly="state == 'processing'"/>
|
||||
<field name="negative_prompt" placeholder="Things to avoid in the image..."
|
||||
readonly="state == 'processing'"/>
|
||||
</group>
|
||||
|
||||
<div class="alert alert-info" role="alert" title="Regenerate" invisible="state not in ('done', 'error')">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
<strong>Want a different image?</strong>
|
||||
Modify the prompt, style, or settings above, then click <strong>Regenerate</strong>.
|
||||
Your current image will be saved as backup.
|
||||
</div>
|
||||
|
||||
<group invisible="state != 'done'">
|
||||
<group string="Result">
|
||||
<field name="revised_prompt" readonly="1"/>
|
||||
<field name="width"/>
|
||||
<field name="height"/>
|
||||
</group>
|
||||
<group string="SEO">
|
||||
<field name="alt_text" placeholder="SEO-friendly alt text"/>
|
||||
<field name="inserted_in_content"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Optimization" invisible="state != 'done'">
|
||||
<group>
|
||||
<field name="is_optimized" widget="boolean_toggle" readonly="1"/>
|
||||
<field name="optimization_quality" invisible="is_optimized"/>
|
||||
<field name="original_size" readonly="1" widget="integer"
|
||||
invisible="not original_size"/>
|
||||
<field name="optimized_size" readonly="1" widget="integer"
|
||||
invisible="not is_optimized"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="size_reduction" readonly="1" widget="progressbar"
|
||||
invisible="not is_optimized"/>
|
||||
<field name="webp_size" readonly="1" widget="integer"
|
||||
invisible="not is_optimized"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Tracking">
|
||||
<group>
|
||||
<field name="task_id" readonly="1"/>
|
||||
<field name="token_cost" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="error_message" readonly="1"
|
||||
invisible="not error_message"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Image Search View -->
|
||||
<record id="otk_seo_content_image_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.image.search</field>
|
||||
<field name="model">otk.seo.content.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Images">
|
||||
<field name="name"/>
|
||||
<field name="content_id"/>
|
||||
<field name="prompt"/>
|
||||
<separator/>
|
||||
<filter name="filter_done" string="Generated"
|
||||
domain="[('state', '=', 'done')]"/>
|
||||
<filter name="filter_pending" string="Pending"
|
||||
domain="[('state', 'in', ('pending', 'processing'))]"/>
|
||||
<filter name="filter_error" string="Failed"
|
||||
domain="[('state', '=', 'error')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_cover" string="Cover Images"
|
||||
domain="[('is_cover', '=', True)]"/>
|
||||
<filter name="filter_optimized" string="Optimized"
|
||||
domain="[('is_optimized', '=', True)]"/>
|
||||
<filter name="filter_not_optimized" string="Not Optimized"
|
||||
domain="[('is_optimized', '=', False), ('state', '=', 'done')]"/>
|
||||
<group>
|
||||
<filter name="group_state" string="Status"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter name="group_style" string="Style"
|
||||
context="{'group_by': 'style'}"/>
|
||||
<filter name="group_content" string="Content"
|
||||
context="{'group_by': 'content_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Image Kanban View -->
|
||||
<record id="otk_seo_content_image_view_kanban" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.image.kanban</field>
|
||||
<field name="model">otk.seo.content.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="name"/>
|
||||
<field name="image"/>
|
||||
<field name="state"/>
|
||||
<field name="is_cover"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="o_kanban_image">
|
||||
<field name="image" widget="image"
|
||||
options="{'size': [100, 100]}"/>
|
||||
</div>
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_title">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="state" widget="badge"/>
|
||||
<span t-if="record.is_cover.raw_value"
|
||||
class="badge text-bg-info ms-1">Cover</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_otk_seo_content_image" model="ir.actions.act_window">
|
||||
<field name="name">Generated Images</field>
|
||||
<field name="res_model">otk.seo.content.image</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="otk_seo_content_image_view_search"/>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No images generated yet
|
||||
</p>
|
||||
<p>
|
||||
Images are automatically generated when you create SEO content
|
||||
with the image generation option enabled.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,488 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- SEO Content List View -->
|
||||
<record id="otk_seo_content_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.list</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="SEO Content" default_order="create_date desc" create="False"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state == 'images_pending' or is_stale"
|
||||
decoration-success="state in ('approved', 'published') and not is_stale"
|
||||
decoration-muted="state == 'archived'">
|
||||
<field name="name"/>
|
||||
<field name="content_type"/>
|
||||
<field name="language_id" optional="show"/>
|
||||
<field name="source_keywords"/>
|
||||
<field name="seo_score" widget="progressbar"
|
||||
options="{'max_value': 100}"/>
|
||||
<field name="word_count" optional="show"/>
|
||||
<field name="image_count" optional="hide"/>
|
||||
<field name="is_stale" string="Stale" widget="boolean"/>
|
||||
<field name="days_since_refresh" optional="hide"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state in ('generating', 'images_pending')"
|
||||
decoration-success="state in ('review', 'approved', 'published')"
|
||||
decoration-danger="state == 'archived'"/>
|
||||
<field name="create_date" optional="hide"/>
|
||||
<field name="blog_post_id" optional="hide"/>
|
||||
<field name="token_cost" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Form View -->
|
||||
<record id="otk_seo_content_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.form</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="SEO Content">
|
||||
<header>
|
||||
<button name="action_generate" type="object" string="Generate"
|
||||
class="oe_highlight" invisible="state != 'draft'"/>
|
||||
<button name="action_regenerate" type="object" string="Regenerate"
|
||||
invisible="state not in ('review', 'approved')"
|
||||
confirm="This will regenerate the content. A version will be saved. Continue?"/>
|
||||
<button name="action_approve" type="object" string="Approve"
|
||||
class="oe_highlight" invisible="state != 'review'"/>
|
||||
<button name="action_create_blog_post" type="object"
|
||||
string="Create Blog Post" class="oe_highlight"
|
||||
invisible="state != 'approved' or blog_post_id"/>
|
||||
<button name="action_apply_to_product" type="object"
|
||||
string="Apply to Product" class="oe_highlight"
|
||||
icon="fa-share"
|
||||
invisible="state not in ('review', 'approved') or content_type != 'product_desc' or not source_product_id"/>
|
||||
<button name="action_back_to_draft" type="object" string="Back to Draft"
|
||||
invisible="state not in ('review', 'approved')"/>
|
||||
<button name="action_insert_images" type="object"
|
||||
string="Insert Images in Content" icon="fa-picture-o"
|
||||
class="btn-secondary"
|
||||
invisible="state not in ('review', 'approved') or not has_insertable_images"/>
|
||||
<button name="action_mark_refreshed" type="object"
|
||||
string="Mark as Refreshed" icon="fa-check"
|
||||
invisible="not is_stale"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,generating,review,approved,published"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_blog_post" type="object"
|
||||
class="oe_stat_button" icon="fa-newspaper-o"
|
||||
invisible="not blog_post_id">
|
||||
<span class="o_stat_text">Blog Post</span>
|
||||
</button>
|
||||
<button name="action_view_images" type="object"
|
||||
class="oe_stat_button" icon="fa-image">
|
||||
<field name="image_count" widget="statinfo" string="Images"/>
|
||||
</button>
|
||||
<button name="action_restore_version" type="object"
|
||||
class="oe_stat_button" icon="fa-history"
|
||||
invisible="current_version <= 1">
|
||||
<field name="current_version" widget="statinfo" string="Versions"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<widget name="web_ribbon" title="Needs Refresh"
|
||||
bg_color="text-bg-info" invisible="not is_stale"/>
|
||||
<widget name="web_ribbon" title="Images Pending"
|
||||
bg_color="text-bg-warning" invisible="not images_pending or is_stale"/>
|
||||
<widget name="web_ribbon" title="Error"
|
||||
bg_color="text-bg-danger" invisible="not error_message or images_pending or is_stale"/>
|
||||
<widget name="web_ribbon" title="Generating"
|
||||
bg_color="text-bg-warning" invisible="state != 'generating'"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Content Title"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Content Settings">
|
||||
<field name="content_type"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="source_keywords" placeholder="keyword1, keyword2, ..."
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="tone"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="target_word_count"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="language_id"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="template_id"
|
||||
domain="[('content_type', '=', content_type)]"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="brand_voice_id"
|
||||
readonly="state != 'draft'"/>
|
||||
</group>
|
||||
<group string="SEO Analysis">
|
||||
<field name="seo_score" widget="progressbar"
|
||||
options="{'max_value': 100}"/>
|
||||
<field name="keyword_density"/>
|
||||
<field name="word_count"/>
|
||||
<field name="readability_score"/>
|
||||
<field name="is_seo_optimized" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Generated Content" name="content">
|
||||
<group>
|
||||
<group>
|
||||
<field name="generated_title" placeholder="Generated title will appear here"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="generated_subtitle" placeholder="Generated subtitle"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="generated_content" placeholder="Generated content will appear here..."/>
|
||||
|
||||
<!-- Inline SEO Summary Panel -->
|
||||
<div class="o_seo_inline_panel mt-3 p-3 border rounded bg-light"
|
||||
invisible="not generated_content">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa fa-line-chart me-2 text-primary" title="SEO Score"/>
|
||||
<strong class="me-2">SEO Score</strong>
|
||||
<field name="seo_score" widget="progressbar"
|
||||
options="{'max_value': 100}" class="flex-grow-1"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa fa-tag me-1"/>
|
||||
<span class="me-1">Title:</span>
|
||||
<field name="meta_title_length" class="me-1"/>
|
||||
<span class="text-muted">/60</span>
|
||||
<span class="o_meta_indicator ms-1"
|
||||
invisible="meta_title_status != 'optimal'">
|
||||
<i class="fa fa-check-circle text-success" title="Optimal length"/>
|
||||
</span>
|
||||
<span class="o_meta_indicator ms-1"
|
||||
invisible="meta_title_status != 'short'">
|
||||
<i class="fa fa-exclamation-circle text-warning" title="Too short"/>
|
||||
</span>
|
||||
<span class="o_meta_indicator ms-1"
|
||||
invisible="meta_title_status != 'long'">
|
||||
<i class="fa fa-exclamation-triangle text-danger" title="Too long"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa fa-file-text-o me-1"/>
|
||||
<span class="me-1">Desc:</span>
|
||||
<field name="meta_desc_length" class="me-1"/>
|
||||
<span class="text-muted">/155</span>
|
||||
<span class="o_meta_indicator ms-1"
|
||||
invisible="meta_desc_status != 'optimal'">
|
||||
<i class="fa fa-check-circle text-success" title="Optimal length"/>
|
||||
</span>
|
||||
<span class="o_meta_indicator ms-1"
|
||||
invisible="meta_desc_status != 'short'">
|
||||
<i class="fa fa-exclamation-circle text-warning" title="Too short"/>
|
||||
</span>
|
||||
<span class="o_meta_indicator ms-1"
|
||||
invisible="meta_desc_status != 'long'">
|
||||
<i class="fa fa-exclamation-triangle text-danger" title="Too long"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fa fa-font me-1"/>
|
||||
<span class="me-1">Words:</span>
|
||||
<field name="word_count"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2" invisible="not seo_improvements">
|
||||
<small class="text-muted">
|
||||
<i class="fa fa-lightbulb-o me-1" title="Suggestions"/>
|
||||
<field name="seo_improvements" class="d-inline" widget="text" readonly="1"/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<group string="Generated teaser/excerpt">
|
||||
<field name="generated_teaser" nolabel="1" colspan="2" placeholder="Generated teaser/excerpt"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="SEO Metadata" name="seo">
|
||||
<group>
|
||||
<group string="Meta Tags">
|
||||
<field name="website_meta_title"
|
||||
placeholder="Max 60 characters recommended"/>
|
||||
<field name="website_meta_description"
|
||||
placeholder="Max 155 characters recommended"/>
|
||||
<field name="website_meta_keywords"/>
|
||||
</group>
|
||||
<group string="URL & Keywords">
|
||||
<field name="seo_name" placeholder="url-friendly-name"/>
|
||||
<field name="generated_keywords"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="SEO Analysis" name="seo_analysis">
|
||||
<field name="seo_analysis_html" nolabel="1"/>
|
||||
<group string="Quick Improvement List" invisible="not seo_improvements">
|
||||
<div class="alert alert-light border" role="alert" colspan="2">
|
||||
<strong><i class="fa fa-lightbulb-o"></i> Suggestions to improve your SEO score:</strong>
|
||||
<field name="seo_improvements" nolabel="1" class="mt-2"
|
||||
widget="text" readonly="1" style="white-space: pre-line;"/>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Raw Analysis Data (Debug)" invisible="1">
|
||||
<field name="seo_analysis" readonly="True" widget="json_pretty" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Images" name="images">
|
||||
<group>
|
||||
<group>
|
||||
<field name="requested_image_count"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="image_style"
|
||||
readonly="state != 'draft'"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="images_pending" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="image_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="image" widget="image"
|
||||
options="{'size': [120, 120]}"/>
|
||||
<field name="name"/>
|
||||
<field name="style"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-warning="state in ('pending', 'processing')"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'error'"/>
|
||||
<field name="is_cover" string="Cover"
|
||||
widget="boolean_toggle"/>
|
||||
<field name="is_og_image" string="OG"
|
||||
widget="boolean_toggle"/>
|
||||
<field name="alt_text"/>
|
||||
<button name="action_generate" type="object"
|
||||
string="Generate" icon="fa-magic"
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_retry" type="object"
|
||||
string="Retry" icon="fa-refresh"
|
||||
invisible="state != 'error'"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
<page string="Source" name="source">
|
||||
<group>
|
||||
<group string="Topic">
|
||||
<field name="source_topic" placeholder="Describe what the content should be about..."
|
||||
readonly="state != 'draft'"/>
|
||||
</group>
|
||||
<group string="Source">
|
||||
<field name="source_product_id"
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="source_text" placeholder="Paste existing content to optimize..."
|
||||
readonly="state != 'draft'"/>
|
||||
<field name="reference_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Publishing" name="publishing">
|
||||
<group>
|
||||
<group string="Blog Settings">
|
||||
<field name="blog_id"/>
|
||||
<field name="blog_post_id" readonly="1"/>
|
||||
</group>
|
||||
<group string="Tracking">
|
||||
<field name="token_cost"/>
|
||||
<field name="generation_time"/>
|
||||
<field name="api_task_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Content Freshness">
|
||||
<field name="last_refreshed_date"/>
|
||||
<field name="refresh_threshold_days"/>
|
||||
<field name="days_since_refresh" readonly="1"/>
|
||||
<field name="is_stale" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Error" invisible="not error_message">
|
||||
<field name="error_message" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Search View -->
|
||||
<record id="otk_seo_content_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.search</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search SEO Content">
|
||||
<field name="name"/>
|
||||
<field name="source_keywords"/>
|
||||
<field name="generated_title"/>
|
||||
<separator/>
|
||||
<filter name="filter_draft" string="Draft"
|
||||
domain="[('state', '=', 'draft')]"/>
|
||||
<filter name="filter_review" string="In Review"
|
||||
domain="[('state', '=', 'review')]"/>
|
||||
<filter name="filter_approved" string="Approved"
|
||||
domain="[('state', '=', 'approved')]"/>
|
||||
<filter name="filter_published" string="Published"
|
||||
domain="[('state', '=', 'published')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_blog_post" string="Blog Posts"
|
||||
domain="[('content_type', '=', 'blog_post')]"/>
|
||||
<filter name="filter_product_desc" string="Product Descriptions"
|
||||
domain="[('content_type', '=', 'product_desc')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_high_seo" string="High SEO Score (70+)"
|
||||
domain="[('seo_score', '>=', 70)]"/>
|
||||
<filter name="filter_needs_work" string="Needs SEO Work (<50)"
|
||||
domain="[('seo_score', '<', 50)]"/>
|
||||
<separator/>
|
||||
<filter name="filter_my_content" string="My Content"
|
||||
domain="[('create_uid', '=', uid)]"/>
|
||||
<filter name="filter_stale" string="Needs Refresh"
|
||||
domain="[('is_stale', '=', True)]"/>
|
||||
<group>
|
||||
<filter name="group_state" string="Status"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter name="group_type" string="Content Type"
|
||||
context="{'group_by': 'content_type'}"/>
|
||||
<filter name="group_language" string="Language"
|
||||
context="{'group_by': 'language_id'}"/>
|
||||
<filter name="group_template" string="Template"
|
||||
context="{'group_by': 'template_id'}"/>
|
||||
<filter name="group_create_date" string="Created"
|
||||
context="{'group_by': 'create_date:month'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Content Kanban View -->
|
||||
<record id="otk_seo_content_view_kanban" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.kanban</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_kanban_small_column">
|
||||
<field name="name"/>
|
||||
<field name="content_type"/>
|
||||
<field name="seo_score"/>
|
||||
<field name="state"/>
|
||||
<field name="image_count"/>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_title">
|
||||
<strong><field name="name"/></strong>
|
||||
</div>
|
||||
<div class="o_kanban_record_subtitle">
|
||||
<field name="content_type"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<span title="SEO Score">
|
||||
<i class="fa fa-line-chart"/> <field name="seo_score"/>%
|
||||
</span>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span title="Images">
|
||||
<i class="fa fa-image"/> <field name="image_count"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Content Version List View -->
|
||||
<record id="otk_seo_content_version_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.version.list</field>
|
||||
<field name="model">otk.seo.content.version</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Version History">
|
||||
<field name="version_number"/>
|
||||
<field name="note"/>
|
||||
<field name="generated_title"/>
|
||||
<field name="created_by"/>
|
||||
<field name="created_at"/>
|
||||
<button name="action_view_diff" type="object"
|
||||
string="View Diff" icon="fa-exchange"
|
||||
class="btn-link"/>
|
||||
<button name="action_restore" type="object"
|
||||
string="Restore" icon="fa-undo"
|
||||
confirm="Restore this version? Current content will be saved as a new version."/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Content Version Diff View (Dialog) -->
|
||||
<record id="otk_seo_content_version_view_diff" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.version.diff</field>
|
||||
<field name="model">otk.seo.content.version</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Version Diff">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="version_number" readonly="1"/>
|
||||
<field name="note" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="created_by" readonly="1"/>
|
||||
<field name="created_at" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Changes from this version to current content"/>
|
||||
<field name="diff_preview" nolabel="1" readonly="1"/>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_restore" type="object"
|
||||
string="Restore This Version" class="btn-primary"
|
||||
confirm="Restore this version? Current content will be saved as a new version."/>
|
||||
<button string="Close" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_otk_seo_content" model="ir.actions.act_window">
|
||||
<field name="name">SEO Content</field>
|
||||
<field name="res_model">otk.seo.content</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
<field name="search_view_id" ref="otk_seo_content_view_search"/>
|
||||
<field name="context">{}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first SEO content!
|
||||
</p>
|
||||
<p>
|
||||
Generate AI-powered blog posts, product descriptions, and more
|
||||
with built-in SEO optimization and image generation.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- OWL Dashboard Client Action (new xmlid - old act_window used action_seo_content_dashboard) -->
|
||||
<record id="action_seo_content_dashboard_owl" model="ir.actions.client">
|
||||
<field name="name">SEO Content Dashboard</field>
|
||||
<field name="tag">seo_content_dashboard</field>
|
||||
</record>
|
||||
|
||||
<!-- Keep act_window for pivot/graph reports -->
|
||||
<record id="action_seo_content_reports" model="ir.actions.act_window">
|
||||
<field name="name">SEO Content Reports</field>
|
||||
<field name="res_model">otk.seo.content</field>
|
||||
<field name="view_mode">pivot,graph,list</field>
|
||||
<field name="context">{'search_default_filter_my_content': 1}</field>
|
||||
</record>
|
||||
<!-- Graph View - Content by Type -->
|
||||
<record id="otk_seo_content_view_graph_type" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.graph.type</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Content by Type" type="pie">
|
||||
<field name="content_type"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Graph View - Content by State -->
|
||||
<record id="otk_seo_content_view_graph_state" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.graph.state</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Content by Status" type="bar">
|
||||
<field name="state"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Graph View - Token Usage Over Time -->
|
||||
<record id="otk_seo_content_view_graph_tokens" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.graph.tokens</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Token Usage Over Time" type="line">
|
||||
<field name="create_date" interval="month"/>
|
||||
<field name="token_cost" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Graph View - SEO Score Distribution -->
|
||||
<record id="otk_seo_content_view_graph_seo" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.graph.seo</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="SEO Score Distribution" type="bar">
|
||||
<field name="seo_score"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Pivot View -->
|
||||
<record id="otk_seo_content_view_pivot" model="ir.ui.view">
|
||||
<field name="name">otk.seo.content.pivot</field>
|
||||
<field name="model">otk.seo.content</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Content Analysis">
|
||||
<field name="content_type" type="row"/>
|
||||
<field name="state" type="col"/>
|
||||
<field name="token_cost" type="measure"/>
|
||||
<field name="word_count" type="measure"/>
|
||||
<field name="seo_score" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Graph Action - Token Usage -->
|
||||
<record id="action_seo_content_token_usage" model="ir.actions.act_window">
|
||||
<field name="name">Token Usage</field>
|
||||
<field name="res_model">otk.seo.content</field>
|
||||
<field name="view_mode">pivot,graph,list</field>
|
||||
<field name="view_id" ref="otk_seo_content_view_graph_tokens"/>
|
||||
</record>
|
||||
|
||||
<!-- Graph Action - Content by Type -->
|
||||
<record id="action_seo_content_by_type" model="ir.actions.act_window">
|
||||
<field name="name">Content by Type</field>
|
||||
<field name="res_model">otk.seo.content</field>
|
||||
<field name="view_mode">pivot,graph,list</field>
|
||||
<field name="view_id" ref="otk_seo_content_view_graph_type"/>
|
||||
</record>
|
||||
|
||||
<!-- Graph Action - Content by State -->
|
||||
<record id="action_seo_content_by_state" model="ir.actions.act_window">
|
||||
<field name="name">Content by Status</field>
|
||||
<field name="res_model">otk.seo.content</field>
|
||||
<field name="view_mode">pivot,graph,list</field>
|
||||
<field name="view_id" ref="otk_seo_content_view_graph_state"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,207 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Reference Tag List View -->
|
||||
<record id="otk_seo_reference_tag_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.reference.tag.list</field>
|
||||
<field name="model">otk.seo.reference.tag</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Reference Tags" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="color" widget="color_picker"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Reference List View -->
|
||||
<record id="otk_seo_reference_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.reference.list</field>
|
||||
<field name="model">otk.seo.reference</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Reference Library"
|
||||
decoration-danger="reference_type == 'url' and url_fetch_status == 'error'">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="reference_type" widget="badge"
|
||||
decoration-info="reference_type == 'internal_content'"
|
||||
decoration-success="reference_type == 'url'"
|
||||
decoration-warning="reference_type == 'text'"
|
||||
decoration-muted="reference_type == 'file'"/>
|
||||
<field name="url_fetch_status" string="URL Status" widget="badge"
|
||||
decoration-info="url_fetch_status == 'pending'"
|
||||
decoration-warning="url_fetch_status == 'fetching'"
|
||||
decoration-success="url_fetch_status == 'success'"
|
||||
decoration-danger="url_fetch_status == 'error'"
|
||||
optional="show"/>
|
||||
<field name="content_type"/>
|
||||
<field name="tag_ids" widget="many2many_tags"
|
||||
options="{'color_field': 'color'}"/>
|
||||
<field name="content_length"/>
|
||||
<field name="active" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Reference Form View -->
|
||||
<record id="otk_seo_reference_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.reference.form</field>
|
||||
<field name="model">otk.seo.reference</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reference">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived"
|
||||
bg_color="text-bg-danger" invisible="active"/>
|
||||
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Reference Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Reference Settings">
|
||||
<field name="reference_type" widget="radio"
|
||||
options="{'horizontal': true}"/>
|
||||
<field name="content_type"/>
|
||||
<field name="sequence"/>
|
||||
<field name="active" invisible="1"/>
|
||||
</group>
|
||||
<group string="Organization">
|
||||
<field name="tag_ids" widget="many2many_tags"
|
||||
options="{'color_field': 'color', 'no_create_edit': False}"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Reference Content">
|
||||
<!-- Internal Content -->
|
||||
<field name="internal_content_id"
|
||||
invisible="reference_type != 'internal_content'"
|
||||
required="reference_type == 'internal_content'"
|
||||
options="{'no_create': True}"/>
|
||||
|
||||
<!-- URL -->
|
||||
<field name="url" widget="url"
|
||||
invisible="reference_type != 'url'"
|
||||
required="reference_type == 'url'"
|
||||
placeholder="https://example.com/article"/>
|
||||
|
||||
<!-- URL Fetch Status -->
|
||||
<div invisible="reference_type != 'url'" class="d-flex align-items-center gap-2 mb-2" colspan="2">
|
||||
<field name="url_fetch_status" widget="badge"
|
||||
decoration-info="url_fetch_status == 'pending'"
|
||||
decoration-warning="url_fetch_status == 'fetching'"
|
||||
decoration-success="url_fetch_status == 'success'"
|
||||
decoration-danger="url_fetch_status == 'error'"/>
|
||||
<field name="url_fetch_date" readonly="1" class="text-muted"/>
|
||||
<button name="action_fetch_url_content" type="object"
|
||||
string="Refresh Content" icon="fa-refresh"
|
||||
class="btn-sm btn-secondary"
|
||||
invisible="not url"/>
|
||||
</div>
|
||||
<field name="url_fetch_error" readonly="1"
|
||||
invisible="reference_type != 'url' or url_fetch_status != 'error'"
|
||||
class="text-danger"/>
|
||||
<field name="url_content_type" readonly="1"
|
||||
invisible="reference_type != 'url' or url_fetch_status != 'success'"/>
|
||||
<field name="url_fetched_content" readonly="1"
|
||||
invisible="reference_type != 'url' or url_fetch_status != 'success'"
|
||||
placeholder="Content will be fetched from URL automatically..."/>
|
||||
|
||||
<!-- Text -->
|
||||
<field name="text_content"
|
||||
invisible="reference_type != 'text'"
|
||||
required="reference_type == 'text'"
|
||||
placeholder="Paste reference text content here..."/>
|
||||
|
||||
<!-- File -->
|
||||
<field name="file" filename="file_name"
|
||||
invisible="reference_type != 'file'"
|
||||
required="reference_type == 'file'"/>
|
||||
<field name="file_name" invisible="1"/>
|
||||
</group>
|
||||
|
||||
<group string="Description">
|
||||
<field name="description" nolabel="1" colspan="2"
|
||||
placeholder="Brief description of this reference and when to use it..."/>
|
||||
</group>
|
||||
|
||||
<group string="Content Preview" invisible="not content_preview">
|
||||
<field name="content_length" string="Content Length (chars)"/>
|
||||
<field name="content_preview" nolabel="1" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Reference Search View -->
|
||||
<record id="otk_seo_reference_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.reference.search</field>
|
||||
<field name="model">otk.seo.reference</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search References">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="tag_ids"/>
|
||||
<separator/>
|
||||
<filter name="filter_internal" string="Internal Content"
|
||||
domain="[('reference_type', '=', 'internal_content')]"/>
|
||||
<filter name="filter_url" string="URLs"
|
||||
domain="[('reference_type', '=', 'url')]"/>
|
||||
<filter name="filter_url_error" string="URL Fetch Errors"
|
||||
domain="[('reference_type', '=', 'url'), ('url_fetch_status', '=', 'error')]"/>
|
||||
<filter name="filter_text" string="Text"
|
||||
domain="[('reference_type', '=', 'text')]"/>
|
||||
<filter name="filter_file" string="Files"
|
||||
domain="[('reference_type', '=', 'file')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_blog" string="For Blog Posts"
|
||||
domain="['|', ('content_type', '=', 'blog_post'), ('content_type', '=', 'all')]"/>
|
||||
<filter name="filter_product" string="For Products"
|
||||
domain="['|', ('content_type', '=', 'product_desc'), ('content_type', '=', 'all')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_active" string="Active"
|
||||
domain="[('active', '=', True)]"/>
|
||||
<filter name="filter_archived" string="Archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
<group>
|
||||
<filter name="group_type" string="Type"
|
||||
context="{'group_by': 'reference_type'}"/>
|
||||
<filter name="group_content_type" string="Content Type"
|
||||
context="{'group_by': 'content_type'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Actions -->
|
||||
<record id="action_otk_seo_reference" model="ir.actions.act_window">
|
||||
<field name="name">Reference Library</field>
|
||||
<field name="res_model">otk.seo.reference</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="otk_seo_reference_view_search"/>
|
||||
<field name="context">{'search_default_filter_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first reference!
|
||||
</p>
|
||||
<p>
|
||||
Build a library of reference materials that AI can use
|
||||
when generating content. Include URLs, text snippets,
|
||||
existing content, or uploaded documents.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_otk_seo_reference_tag" model="ir.actions.act_window">
|
||||
<field name="name">Reference Tags</field>
|
||||
<field name="res_model">otk.seo.reference.tag</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create tags to organize references!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,189 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- SEO Template List View -->
|
||||
<record id="otk_seo_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">otk.seo.template.list</field>
|
||||
<field name="model">otk.seo.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Content Templates">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="content_type"/>
|
||||
<field name="default_tone"/>
|
||||
<field name="default_word_count"/>
|
||||
<field name="include_images" widget="boolean_toggle"/>
|
||||
<field name="usage_count"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Template Form View -->
|
||||
<record id="otk_seo_template_view_form" model="ir.ui.view">
|
||||
<field name="name">otk.seo.template.form</field>
|
||||
<field name="model">otk.seo.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Content Template">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_contents" type="object"
|
||||
class="oe_stat_button" icon="fa-file-text-o">
|
||||
<field name="usage_count" widget="statinfo" string="Uses"/>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived"
|
||||
bg_color="text-bg-danger" invisible="active"/>
|
||||
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Template Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Basic Settings">
|
||||
<field name="content_type"/>
|
||||
<field name="sequence"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group string="Defaults">
|
||||
<field name="default_tone"/>
|
||||
<field name="default_word_count"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<field name="description" placeholder="Describe when to use this template..."/>
|
||||
|
||||
<notebook>
|
||||
<page string="Prompts" name="prompts">
|
||||
<group string="System Prompt">
|
||||
<field name="system_prompt" nolabel="1" colspan="2"
|
||||
placeholder="Instructions for the AI about style, format, constraints...
|
||||
|
||||
Example:
|
||||
You are an expert content writer specializing in {industry}.
|
||||
Write in a {tone} style, using clear and concise language.
|
||||
Structure the content with clear headers and bullet points where appropriate."/>
|
||||
</group>
|
||||
<group string="Content Prompt Template">
|
||||
<field name="content_prompt_template" nolabel="1" colspan="2"
|
||||
placeholder="Template for the main content.
|
||||
|
||||
Available placeholders:
|
||||
- {keywords} - Target keywords
|
||||
- {topic} - Topic description
|
||||
- {product_name} - Product name (if applicable)
|
||||
- {tone} - Selected tone
|
||||
- {word_count} - Target word count
|
||||
- {language} - Target language
|
||||
|
||||
Example:
|
||||
Write a comprehensive {word_count} word blog post about {topic}.
|
||||
Target keywords: {keywords}
|
||||
Tone: {tone}"/>
|
||||
</group>
|
||||
<group string="Meta Description Prompt">
|
||||
<field name="meta_prompt_template" nolabel="1" colspan="2"
|
||||
placeholder="Template for generating meta description..."/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Image Settings" name="images">
|
||||
<group>
|
||||
<group string="Image Generation">
|
||||
<field name="include_images" widget="boolean_toggle"/>
|
||||
<field name="default_image_count"
|
||||
invisible="not include_images"/>
|
||||
<field name="default_image_style"
|
||||
invisible="not include_images"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Image Prompt Template" invisible="not include_images">
|
||||
<field name="image_prompt_template" nolabel="1" colspan="2"
|
||||
placeholder="Template for image generation.
|
||||
|
||||
Available placeholders:
|
||||
- {title} - Content title
|
||||
- {keywords} - Target keywords
|
||||
- {section} - Section being illustrated
|
||||
|
||||
Example:
|
||||
Professional photograph illustrating {title}.
|
||||
Style: Modern, clean, business-appropriate.
|
||||
Related to: {keywords}"/>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Formatting" name="formatting">
|
||||
<group>
|
||||
<group string="Content Structure">
|
||||
<field name="header_structure"/>
|
||||
<field name="include_toc"/>
|
||||
<field name="include_faq"/>
|
||||
<field name="include_cta"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="References" name="references">
|
||||
<group>
|
||||
<field name="reference_ids" widget="many2many_tags"
|
||||
domain="['|', ('content_type', '=', content_type), ('content_type', '=', 'all')]"
|
||||
placeholder="Select default references for this template..."/>
|
||||
</group>
|
||||
<p class="text-muted">
|
||||
References selected here will be automatically added when users
|
||||
select this template. They can still add or remove references before generating.
|
||||
</p>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEO Template Search View -->
|
||||
<record id="otk_seo_template_view_search" model="ir.ui.view">
|
||||
<field name="name">otk.seo.template.search</field>
|
||||
<field name="model">otk.seo.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Templates">
|
||||
<field name="name"/>
|
||||
<field name="content_type"/>
|
||||
<separator/>
|
||||
<filter name="filter_blog" string="Blog Post"
|
||||
domain="[('content_type', '=', 'blog_post')]"/>
|
||||
<filter name="filter_product" string="Product Description"
|
||||
domain="[('content_type', '=', 'product_desc')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_active" string="Active"
|
||||
domain="[('active', '=', True)]"/>
|
||||
<filter name="filter_archived" string="Archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
<group>
|
||||
<filter name="group_type" string="Content Type"
|
||||
context="{'group_by': 'content_type'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_otk_seo_template" model="ir.actions.act_window">
|
||||
<field name="name">Content Templates</field>
|
||||
<field name="res_model">otk.seo.template</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="otk_seo_template_view_search"/>
|
||||
<field name="context">{'search_default_filter_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first content template!
|
||||
</p>
|
||||
<p>
|
||||
Templates help maintain consistent style and formatting
|
||||
across your generated content.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user