Refactor: Move SEO content files to root of module

This commit is contained in:
2026-07-12 07:08:33 +02:00
parent 8e01c7c8ec
commit 45c65e24dd
99 changed files with 0 additions and 3739 deletions
@@ -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>
+108
View File
@@ -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&#10;keyword2, topic2&#10;..."/>
</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 &lt;= 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 &amp; 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 (&lt;50)"
domain="[('seo_score', '&lt;', 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>