Restructure project files to follow the addon layout: - Move models to addons/itsulu_blog_publisher/models/ - Move services (LLM providers, routers) to addons/itsulu_blog_publisher/services/ - Move wizards to addons/itsulu_blog_publisher/wizards/ - Move views (XML templates) to addons/itsulu_blog_publisher/views/ - Move data (cron, mail templates) to addons/itsulu_blog_publisher/data/ - Move security (ACL) to addons/itsulu_blog_publisher/security/ - Move tests and factories to addons/itsulu_blog_publisher/tests/ - Move BDD features to addons/itsulu_blog_publisher/features/ - Create __init__.py files for all Python packages This enables proper Odoo module discovery and import structure. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
439 lines
22 KiB
XML
439 lines
22 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
||
<odoo>
|
||
|
||
<!-- ================================================================
|
||
TOPIC QUEUE VIEWS
|
||
================================================================ -->
|
||
|
||
<record id="view_blog_topic_tree" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.topic.tree</field>
|
||
<field name="model">itsulu.blog.topic</field>
|
||
<field name="arch" type="xml">
|
||
<tree string="Topic Queue" decoration-muted="state=='used'" decoration-warning="priority=='urgent'">
|
||
<field name="sequence" widget="handle"/>
|
||
<field name="priority" widget="priority"/>
|
||
<field name="name"/>
|
||
<field name="blog_id"/>
|
||
<field name="state" widget="badge"
|
||
decoration-success="state=='pending'"
|
||
decoration-info="state=='used'"
|
||
decoration-warning="state=='skipped'"/>
|
||
<field name="used_date" optional="show"/>
|
||
<button name="action_mark_pending" type="object" string="↩ Reset"
|
||
attrs="{'invisible': [('state', '=', 'pending')]}"
|
||
class="btn-sm btn-secondary"/>
|
||
<button name="action_mark_skipped" type="object" string="Skip"
|
||
attrs="{'invisible': [('state', '!=', 'pending')]}"
|
||
class="btn-sm btn-warning"/>
|
||
</tree>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="view_blog_topic_form" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.topic.form</field>
|
||
<field name="model">itsulu.blog.topic</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Blog Topic">
|
||
<header>
|
||
<button name="action_mark_pending" type="object" string="Reset to Pending"
|
||
attrs="{'invisible': [('state', '=', 'pending')]}"
|
||
class="btn-secondary"/>
|
||
<button name="action_mark_skipped" type="object" string="Skip"
|
||
attrs="{'invisible': [('state', '!=', 'pending')]}"
|
||
class="btn-warning"/>
|
||
<field name="state" widget="statusbar" statusbar_visible="pending,used"/>
|
||
</header>
|
||
<sheet>
|
||
<group>
|
||
<group string="Topic">
|
||
<field name="name" placeholder="e.g. How ITSulu Helps SMBs Migrate to the Cloud"/>
|
||
<field name="priority" widget="priority"/>
|
||
<field name="sequence"/>
|
||
<field name="blog_id"/>
|
||
<field name="tone"/>
|
||
</group>
|
||
<group string="Status">
|
||
<field name="state"/>
|
||
<field name="used_date" readonly="1"/>
|
||
<field name="generation_log_id" readonly="1"/>
|
||
</group>
|
||
</group>
|
||
<notebook>
|
||
<page string="Notes / Context for LLM">
|
||
<field name="notes" placeholder="Additional context: specific services to highlight, recent news, preferred angle, key statistics..."/>
|
||
</page>
|
||
</notebook>
|
||
</sheet>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="action_blog_topic_list" model="ir.actions.act_window">
|
||
<field name="name">Topic Queue</field>
|
||
<field name="res_model">itsulu.blog.topic</field>
|
||
<field name="view_mode">tree,form</field>
|
||
<field name="context">{'search_default_state_pending': 1}</field>
|
||
</record>
|
||
|
||
<record id="view_blog_topic_search" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.topic.search</field>
|
||
<field name="model">itsulu.blog.topic</field>
|
||
<field name="arch" type="xml">
|
||
<search>
|
||
<field name="name"/>
|
||
<filter name="state_pending" string="Pending" domain="[('state','=','pending')]"/>
|
||
<filter name="state_used" string="Used" domain="[('state','=','used')]"/>
|
||
<filter name="priority_urgent" string="Urgent" domain="[('priority','=','urgent')]"/>
|
||
<group expand="0" string="Group By">
|
||
<filter name="group_state" string="State" context="{'group_by': 'state'}"/>
|
||
<filter name="group_priority" string="Priority" context="{'group_by': 'priority'}"/>
|
||
<filter name="group_blog" string="Blog" context="{'group_by': 'blog_id'}"/>
|
||
</group>
|
||
</search>
|
||
</field>
|
||
</record>
|
||
|
||
<!-- ================================================================
|
||
SCHEDULE SLOT VIEWS
|
||
================================================================ -->
|
||
|
||
<record id="view_blog_schedule_tree" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.schedule.tree</field>
|
||
<field name="model">itsulu.blog.schedule</field>
|
||
<field name="arch" type="xml">
|
||
<tree string="Schedule Slots">
|
||
<field name="name"/>
|
||
<field name="slot"/>
|
||
<field name="trigger_time"/>
|
||
<field name="blog_id"/>
|
||
<field name="llm_provider"/>
|
||
<field name="llm_model"/>
|
||
<field name="image_provider"/>
|
||
<field name="auto_publish" widget="toggle_button"/>
|
||
<field name="active" widget="toggle_button"/>
|
||
<field name="last_run" optional="show"/>
|
||
<button name="%(action_blog_generate_wizard)d" type="action"
|
||
string="▶ Run Now" class="btn-sm btn-primary"
|
||
context="{'default_blog_id': blog_id, 'default_llm_provider': llm_provider, 'default_llm_model': llm_model}"/>
|
||
</tree>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="view_blog_schedule_form" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.schedule.form</field>
|
||
<field name="model">itsulu.blog.schedule</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Schedule Slot">
|
||
<header>
|
||
<button name="%(action_blog_generate_wizard)d" type="action"
|
||
string="▶ Run Now" class="btn-primary"/>
|
||
</header>
|
||
<sheet>
|
||
<widget name="web_ribbon" title="Inactive" bg_color="bg-danger"
|
||
attrs="{'invisible': [('active', '=', True)]}"/>
|
||
<div class="oe_title">
|
||
<h1><field name="name" placeholder="e.g. Morning Post"/></h1>
|
||
</div>
|
||
<group>
|
||
<group string="Timing">
|
||
<field name="slot"/>
|
||
<field name="trigger_time"/>
|
||
<field name="active"/>
|
||
</group>
|
||
<group string="Content">
|
||
<field name="blog_id"/>
|
||
<field name="tone"/>
|
||
<field name="auto_publish"/>
|
||
</group>
|
||
</group>
|
||
<notebook>
|
||
<page string="LLM Configuration">
|
||
<group>
|
||
<group string="Text Generation">
|
||
<field name="llm_provider"/>
|
||
<field name="llm_model"/>
|
||
</group>
|
||
<group string="Image Generation">
|
||
<field name="image_provider"/>
|
||
</group>
|
||
</group>
|
||
</page>
|
||
<page string="Social Media Platforms">
|
||
<p class="text-muted">Enable or disable social media copy generation for this slot.</p>
|
||
<group>
|
||
<field name="platform_twitter" widget="toggle_button"/>
|
||
<field name="platform_bluesky" widget="toggle_button"/>
|
||
<field name="platform_mastodon" widget="toggle_button"/>
|
||
<field name="platform_linkedin" widget="toggle_button"/>
|
||
</group>
|
||
</page>
|
||
<page string="Prompt Override">
|
||
<p class="text-muted">Leave blank to use the global prompt template from Settings. Enter a custom prompt to override for this slot only.</p>
|
||
<field name="prompt_override" placeholder="Optional: custom user prompt for this slot..."/>
|
||
</page>
|
||
<page string="Notification">
|
||
<field name="notification_emails"
|
||
placeholder="Leave blank to use global Settings (nicholasr@itsulu.com,sales@itsulu.com)"/>
|
||
</page>
|
||
<page string="Generation Log">
|
||
<field name="log_ids" readonly="1">
|
||
<tree decoration-danger="state=='error'" decoration-success="state=='success'">
|
||
<field name="create_date"/>
|
||
<field name="state" widget="badge"/>
|
||
<field name="topic_used"/>
|
||
<field name="tokens_used"/>
|
||
<field name="duration_seconds" string="Duration (s)"/>
|
||
<field name="blog_post_id"/>
|
||
</tree>
|
||
</field>
|
||
</page>
|
||
</notebook>
|
||
</sheet>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="action_blog_schedule_list" model="ir.actions.act_window">
|
||
<field name="name">Schedule Slots</field>
|
||
<field name="res_model">itsulu.blog.schedule</field>
|
||
<field name="view_mode">tree,form</field>
|
||
</record>
|
||
|
||
<!-- ================================================================
|
||
GENERATION LOG VIEWS
|
||
================================================================ -->
|
||
|
||
<record id="view_blog_generation_log_tree" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.generation.log.tree</field>
|
||
<field name="model">itsulu.blog.generation.log</field>
|
||
<field name="arch" type="xml">
|
||
<tree string="Generation Log"
|
||
decoration-danger="state=='error'"
|
||
decoration-success="state=='success'"
|
||
decoration-info="state=='running'">
|
||
<field name="create_date" string="Date"/>
|
||
<field name="state" widget="badge"
|
||
decoration-success="state=='success'"
|
||
decoration-danger="state=='error'"
|
||
decoration-info="state=='running'"/>
|
||
<field name="trigger_source"/>
|
||
<field name="schedule_slot" optional="show"/>
|
||
<field name="topic_used"/>
|
||
<field name="llm_provider"/>
|
||
<field name="llm_model" optional="show"/>
|
||
<field name="tokens_used"/>
|
||
<field name="duration_seconds" string="Dur (s)" optional="show"/>
|
||
<field name="blog_post_id"/>
|
||
<button name="action_retry" type="object" string="↩ Retry"
|
||
attrs="{'invisible': [('state', '!=', 'error')]}"
|
||
class="btn-sm btn-warning"
|
||
data-test-id="btn-retry-generation"/>
|
||
</tree>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="view_blog_generation_log_form" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.generation.log.form</field>
|
||
<field name="model">itsulu.blog.generation.log</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Generation Log" create="false" edit="false">
|
||
<header>
|
||
<button name="action_retry" type="object" string="↩ Retry Generation"
|
||
attrs="{'invisible': [('state', '!=', 'error')]}"
|
||
class="btn-warning"
|
||
data-test-id="btn-retry-log-form"/>
|
||
</header>
|
||
<sheet>
|
||
<group>
|
||
<group string="Result">
|
||
<field name="state" widget="badge"/>
|
||
<field name="blog_post_id"/>
|
||
<field name="error_message" attrs="{'invisible': [('state', '!=', 'error')]}"/>
|
||
</group>
|
||
<group string="Trigger">
|
||
<field name="trigger_source"/>
|
||
<field name="schedule_slot"/>
|
||
<field name="topic_used"/>
|
||
<field name="topic_source"/>
|
||
<field name="triggered_by"/>
|
||
</group>
|
||
</group>
|
||
<group>
|
||
<group string="LLM">
|
||
<field name="llm_provider"/>
|
||
<field name="llm_model"/>
|
||
<field name="image_provider"/>
|
||
<field name="tokens_used"/>
|
||
<field name="duration_seconds"/>
|
||
</group>
|
||
<group string="Social Platforms">
|
||
<field name="platform_twitter"/>
|
||
<field name="platform_bluesky"/>
|
||
<field name="platform_mastodon"/>
|
||
<field name="platform_linkedin"/>
|
||
</group>
|
||
</group>
|
||
<notebook>
|
||
<page string="Raw LLM Response" groups="base.group_system">
|
||
<field name="raw_llm_response" readonly="1"/>
|
||
</page>
|
||
</notebook>
|
||
</sheet>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="action_blog_generation_log_list" model="ir.actions.act_window">
|
||
<field name="name">Generation Log</field>
|
||
<field name="res_model">itsulu.blog.generation.log</field>
|
||
<field name="view_mode">tree,form</field>
|
||
<field name="context">{'search_default_state_group': 1}</field>
|
||
</record>
|
||
|
||
<!-- ================================================================
|
||
SOCIAL MEDIA COPY VIEWS
|
||
================================================================ -->
|
||
|
||
<record id="view_blog_post_social_tree" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.post.social.tree</field>
|
||
<field name="model">itsulu.blog.post.social</field>
|
||
<field name="arch" type="xml">
|
||
<tree string="Social Media Copy">
|
||
<field name="blog_post_id"/>
|
||
<field name="twitter_enabled" widget="boolean_toggle" optional="show"/>
|
||
<field name="bluesky_enabled" widget="boolean_toggle" optional="show"/>
|
||
<field name="mastodon_enabled" widget="boolean_toggle" optional="show"/>
|
||
<field name="linkedin_enabled" widget="boolean_toggle" optional="show"/>
|
||
</tree>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="view_blog_post_social_form" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.post.social.form</field>
|
||
<field name="model">itsulu.blog.post.social</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Social Media Copy">
|
||
<sheet>
|
||
<group>
|
||
<field name="blog_post_id" readonly="1"/>
|
||
</group>
|
||
<notebook>
|
||
<page string="🐦 X / Twitter">
|
||
<group>
|
||
<field name="twitter_enabled" widget="toggle_button"/>
|
||
</group>
|
||
<separator string="Post A"/>
|
||
<field name="twitter_post_a" data-test-id="twitter-post-a"
|
||
placeholder="X/Twitter Post A (max 280 chars)"/>
|
||
<separator string="Post B"/>
|
||
<field name="twitter_post_b" data-test-id="twitter-post-b"
|
||
placeholder="X/Twitter Post B (max 280 chars)"/>
|
||
</page>
|
||
<page string="🌐 BlueSky">
|
||
<group>
|
||
<field name="bluesky_enabled" widget="toggle_button"/>
|
||
</group>
|
||
<separator string="Post A"/>
|
||
<field name="bluesky_post_a" data-test-id="bluesky-post-a"
|
||
placeholder="BlueSky Post A (max 300 chars)"/>
|
||
<separator string="Post B"/>
|
||
<field name="bluesky_post_b" data-test-id="bluesky-post-b"
|
||
placeholder="BlueSky Post B (max 300 chars)"/>
|
||
</page>
|
||
<page string="🦣 Mastodon">
|
||
<group>
|
||
<field name="mastodon_enabled" widget="toggle_button"/>
|
||
</group>
|
||
<field name="mastodon_post" data-test-id="mastodon-post"
|
||
placeholder="Mastodon/Fediverse post (max 500 chars)"/>
|
||
</page>
|
||
<page string="💼 LinkedIn">
|
||
<group>
|
||
<field name="linkedin_enabled" widget="toggle_button"/>
|
||
</group>
|
||
<field name="linkedin_post" data-test-id="linkedin-post"
|
||
placeholder="LinkedIn post (150–3000 chars)"/>
|
||
</page>
|
||
<page string="Sources Referenced">
|
||
<field name="sources_referenced" data-test-id="sources-referenced"
|
||
placeholder="Title — https://url.com"/>
|
||
</page>
|
||
</notebook>
|
||
</sheet>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="action_blog_post_social_list" model="ir.actions.act_window">
|
||
<field name="name">Social Media Copy</field>
|
||
<field name="res_model">itsulu.blog.post.social</field>
|
||
<field name="view_mode">tree,form</field>
|
||
</record>
|
||
|
||
<!-- ================================================================
|
||
GENERATE NOW WIZARD VIEW
|
||
================================================================ -->
|
||
|
||
<record id="view_blog_generate_wizard_form" model="ir.ui.view">
|
||
<field name="name">itsulu.blog.generate.wizard.form</field>
|
||
<field name="model">itsulu.blog.generate.wizard</field>
|
||
<field name="arch" type="xml">
|
||
<form string="Generate Blog Post">
|
||
<sheet>
|
||
<div class="oe_title">
|
||
<h2>AI Blog Post Generator</h2>
|
||
<p class="text-muted">Fill in the fields below and click Generate. One API call creates the full post, SEO, tags, and social media copy.</p>
|
||
</div>
|
||
<group>
|
||
<group string="Content">
|
||
<field name="topic" placeholder="Leave blank to use queue / let AI choose"
|
||
data-test-id="wizard-topic"/>
|
||
<field name="blog_id" data-test-id="wizard-blog"/>
|
||
<field name="tone" data-test-id="wizard-tone"/>
|
||
<field name="auto_publish" widget="toggle_button"
|
||
data-test-id="wizard-auto-publish"/>
|
||
</group>
|
||
<group string="LLM Configuration">
|
||
<field name="llm_provider" data-test-id="wizard-provider"/>
|
||
<field name="llm_model" data-test-id="wizard-model"
|
||
placeholder="e.g. claude-sonnet-4-20250514"/>
|
||
<field name="image_provider" data-test-id="wizard-image-provider"/>
|
||
</group>
|
||
</group>
|
||
<group string="Social Media Platforms">
|
||
<p class="text-muted col-12">Select which platforms to generate copy for:</p>
|
||
<field name="platform_twitter" widget="toggle_button"/>
|
||
<field name="platform_bluesky" widget="toggle_button"/>
|
||
<field name="platform_mastodon" widget="toggle_button"/>
|
||
<field name="platform_linkedin" widget="toggle_button"/>
|
||
</group>
|
||
</sheet>
|
||
<footer>
|
||
<button name="action_generate" type="object" string="▶ Generate Post"
|
||
class="btn-primary" data-test-id="btn-generate-now"/>
|
||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||
</footer>
|
||
</form>
|
||
</field>
|
||
</record>
|
||
|
||
<record id="action_blog_generate_wizard" model="ir.actions.act_window">
|
||
<field name="name">Generate Blog Post</field>
|
||
<field name="res_model">itsulu.blog.generate.wizard</field>
|
||
<field name="view_mode">form</field>
|
||
<field name="target">new</field>
|
||
</record>
|
||
|
||
<!-- ================================================================
|
||
SETTINGS ACTION
|
||
================================================================ -->
|
||
|
||
<record id="action_blog_publisher_settings" model="ir.actions.act_window">
|
||
<field name="name">Blog Publisher Settings</field>
|
||
<field name="res_model">res.config.settings</field>
|
||
<field name="view_mode">form</field>
|
||
<field name="target">inline</field>
|
||
<field name="context">{'module': 'itsulu_blog_publisher'}</field>
|
||
</record>
|
||
|
||
</odoo>
|