itsulu-blog-publisher/addons/itsulu_blog_publisher/views/blog_topic_views.xml
Nicholas Riegel 0fc4febabf Reorganize codebase into Odoo addon structure per ARCHITECTURE.md
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>
2026-05-29 12:11:42 -04:00

439 lines
22 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 (1503000 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>