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
+189
View File
@@ -0,0 +1,189 @@
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class OtkSeoBrandVoice(models.Model):
_name = 'otk.seo.brand.voice'
_description = 'Brand Voice Guidelines'
_order = 'sequence, name'
name = fields.Char('Brand Name', required=True, translate=True,
help='Name of the brand or voice profile (e.g., "Company Main Brand", "Product Line X")')
sequence = fields.Integer('Sequence', default=10)
active = fields.Boolean('Active', default=True)
is_default = fields.Boolean('Default Voice',
help='If enabled, this voice will be automatically selected for new content.')
# === Brand Identity ===
brand_description = fields.Text('Brand Description', translate=True,
help='Brief description of the brand, its values, and target audience. '
'Example: "We are a modern tech company targeting young professionals..."')
target_audience = fields.Text('Target Audience', translate=True,
help='Describe the ideal reader/customer. '
'Example: "Small business owners aged 30-50, tech-savvy but time-constrained..."')
# === Voice Characteristics ===
voice_tone = fields.Selection([
('formal', 'Formal & Professional'),
('friendly', 'Friendly & Approachable'),
('authoritative', 'Authoritative & Expert'),
('conversational', 'Conversational & Casual'),
('inspirational', 'Inspirational & Motivating'),
('educational', 'Educational & Informative'),
('playful', 'Playful & Fun'),
('empathetic', 'Empathetic & Supportive'),
], string='Voice Tone', default='friendly',
help='The overall tone of voice to use in content.')
voice_personality = fields.Char('Personality Traits',
help='Comma-separated personality traits. '
'Example: "innovative, trustworthy, approachable, knowledgeable"')
writing_style = fields.Selection([
('concise', 'Concise & Direct'),
('detailed', 'Detailed & Thorough'),
('storytelling', 'Storytelling & Narrative'),
('factual', 'Factual & Data-driven'),
('persuasive', 'Persuasive & Sales-oriented'),
('conversational', 'Conversational & Casual'),
], string='Writing Style', default='concise',
help='The preferred writing style for content.')
# === Vocabulary Guidelines ===
preferred_words = fields.Text('Preferred Words & Phrases', translate=True,
help='Words and phrases to use frequently. One per line. '
'Example:\ninnovative\ncutting-edge\nseamless experience')
avoided_words = fields.Text('Words to Avoid', translate=True,
help='Words and phrases to never use. One per line. '
'Example:\ncheap\nbasic\njust')
industry_terms = fields.Text('Industry Terminology', translate=True,
help='Industry-specific terms and their preferred usage. '
'Example:\nSaaS (not "software as a service")\nAI-powered (not "artificial intelligence powered")')
# === Custom Instructions ===
custom_instructions = fields.Text('Custom AI Instructions', translate=True,
help='Additional instructions for the AI when generating content. '
'Example: "Always include a call-to-action at the end. '
'Reference sustainability when relevant. Never make claims without evidence."')
# === Examples ===
example_good = fields.Text('Example of Good Content',
help='Paste an example of content that represents your brand voice well.')
example_bad = fields.Text('Example of What to Avoid',
help='Paste an example of content that does NOT match your brand voice.')
# === Usage Tracking ===
content_count = fields.Integer('Content Count', compute='_compute_content_count',
help='Number of content pieces using this brand voice.')
@api.depends('name')
def _compute_content_count(self):
if not self.ids:
return
data = self.env['otk.seo.content']._read_group(
[('brand_voice_id', 'in', self.ids)],
['brand_voice_id'],
['__count'],
)
counts = {voice.id: count for voice, count in data}
for record in self:
record.content_count = counts.get(record.id, 0)
@api.constrains('is_default')
def _check_single_default(self):
"""Ensure only one brand voice is set as default."""
for record in self:
if record.is_default:
existing_default = self.search([
('is_default', '=', True),
('id', '!=', record.id),
('active', '=', True)
])
if existing_default:
existing_default.write({'is_default': False})
@api.model
def get_default_voice(self):
"""Get the default brand voice, if any."""
return self.search([('is_default', '=', True), ('active', '=', True)], limit=1)
def get_prompt_instructions(self):
"""Generate AI instructions from brand voice settings."""
self.ensure_one()
instructions = []
# Brand identity
if self.brand_description:
instructions.append(f"Brand Context: {self.brand_description}")
if self.target_audience:
instructions.append(f"Target Audience: {self.target_audience}")
# Voice characteristics
tone_descriptions = {
'formal': 'Use a formal, professional tone. Avoid slang and casual expressions.',
'friendly': 'Use a friendly, approachable tone. Be warm but professional.',
'authoritative': 'Write with authority and expertise. Be confident and assertive.',
'conversational': 'Write in a conversational, casual tone. Use contractions and relatable language.',
'inspirational': 'Write in an inspirational, motivating tone. Use uplifting language.',
'educational': 'Write in an educational, informative tone. Explain concepts clearly.',
'playful': 'Write in a playful, fun tone. Use humor where appropriate.',
'empathetic': 'Write with empathy and understanding. Acknowledge reader challenges.',
}
if self.voice_tone:
instructions.append(f"Voice Tone: {tone_descriptions.get(self.voice_tone, self.voice_tone)}")
if self.voice_personality:
instructions.append(f"Personality Traits: The content should feel {self.voice_personality}.")
style_descriptions = {
'concise': 'Keep writing concise and direct. Avoid unnecessary words.',
'detailed': 'Provide detailed, thorough explanations. Be comprehensive.',
'storytelling': 'Use storytelling and narrative techniques. Engage emotionally.',
'factual': 'Focus on facts and data. Support claims with evidence.',
'persuasive': 'Write persuasively. Focus on benefits and calls-to-action.',
}
if self.writing_style:
instructions.append(f"Writing Style: {style_descriptions.get(self.writing_style, self.writing_style)}")
# Vocabulary
if self.preferred_words:
words = [w.strip() for w in self.preferred_words.split('\n') if w.strip()]
if words:
instructions.append(f"Preferred vocabulary (use these when appropriate): {', '.join(words[:15])}")
if self.avoided_words:
words = [w.strip() for w in self.avoided_words.split('\n') if w.strip()]
if words:
instructions.append(f"Words to AVOID: {', '.join(words[:15])}")
if self.industry_terms:
instructions.append(f"Industry terminology: {self.industry_terms}")
# Custom instructions
if self.custom_instructions:
instructions.append(f"Additional guidelines: {self.custom_instructions}")
# Examples
if self.example_good:
instructions.append(f"Example of desired content style:\n{self.example_good[:500]}")
return '\n\n'.join(instructions)
def action_view_content(self):
"""View content using this brand voice."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Content with %s Voice') % self.name,
'res_model': 'otk.seo.content',
'view_mode': 'list,form',
'domain': [('brand_voice_id', '=', self.id)],
'context': {'default_brand_voice_id': self.id},
}