# -*- coding: utf-8 -*- """ itsulu.blog.topic — Topic queue for AI blog post generation. Topics come from ITSulu's (or the deploying company's) service portfolio. Users curate this list in the backend; the scheduler pops the highest-priority pending topic for each generation run. """ from odoo import api, fields, models from odoo.exceptions import ValidationError class BlogTopic(models.Model): _name = 'itsulu.blog.topic' _description = 'Blog Publisher — Topic Queue' _order = 'priority_order asc, sequence asc, id asc' # ------------------------------------------------------------------ # # Fields # # ------------------------------------------------------------------ # name = fields.Char( string='Topic / Title Hint', required=True, help='What the blog post should be about. Can be a title, a question, ' 'or a brief description. The LLM will elaborate from this.', ) sequence = fields.Integer( string='Sequence', default=10, help='Lower number = picked first within the same priority level.', ) priority = fields.Selection( selection=[ ('low', 'Low'), ('normal', 'Normal'), ('high', 'High'), ('urgent', 'Urgent'), ], string='Priority', default='normal', required=True, index=True, ) # Numeric equivalent for clean ordering in _order priority_order = fields.Integer( string='Priority Order', compute='_compute_priority_order', store=True, help='Computed from priority for clean DB ordering.', ) state = fields.Selection( selection=[ ('pending', 'Pending'), ('used', 'Used'), ('skipped', 'Skipped'), ], string='State', default='pending', required=True, index=True, ) blog_id = fields.Many2one( comodel_name='blog.blog', string='Target Blog', ondelete='set null', help='Optional — force this topic to publish to a specific blog. ' 'If blank, the schedule slot\'s configured blog is used.', ) notes = fields.Text( string='Notes / Context', help='Additional context for the LLM — e.g. specific services to highlight, ' 'recent news to reference, or a preferred angle.', ) tone = fields.Char( string='Tone Override', help='E.g. "technical", "beginner-friendly", "thought leadership". ' 'Overrides the schedule slot\'s default tone.', ) used_date = fields.Datetime( string='Used On', readonly=True, help='Timestamp when this topic was sent to the LLM.', ) generation_log_id = fields.Many2one( comodel_name='itsulu.blog.generation.log', string='Generation Log', readonly=True, ondelete='set null', ) # ------------------------------------------------------------------ # # Computed fields # # ------------------------------------------------------------------ # @api.depends('priority') def _compute_priority_order(self): _map = {'urgent': 1, 'high': 2, 'normal': 3, 'low': 4} for rec in self: rec.priority_order = _map.get(rec.priority, 3) # ------------------------------------------------------------------ # # Constraints # # ------------------------------------------------------------------ # @api.constrains('name') def _check_name_not_empty(self): for rec in self: if not (rec.name or '').strip(): raise ValidationError("Topic name cannot be empty.") # ------------------------------------------------------------------ # # Business logic # # ------------------------------------------------------------------ # @api.model def get_next_topic(self): """ Return the single highest-priority pending topic, or an empty recordset. Priority order: urgent > high > normal > low. Within the same priority, sequence ASC then id ASC. """ return self.search([('state', '=', 'pending')], limit=1) def mark_used(self, generation_log=None): """Set state to 'used' and record the timestamp.""" self.ensure_one() vals = { 'state': 'used', 'used_date': fields.Datetime.now(), } if generation_log: vals['generation_log_id'] = generation_log.id self.write(vals) def action_mark_pending(self): """Reset a used/skipped topic back to pending for re-use.""" self.write({'state': 'pending', 'used_date': False, 'generation_log_id': False}) def action_mark_skipped(self): self.write({'state': 'skipped'})