121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
import logging
|
|
from odoo import models, fields, api
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FusionAccountingRule(models.Model):
|
|
_name = 'fusion.accounting.rule'
|
|
_description = 'Fusion Accounting Rule'
|
|
_order = 'sequence, id'
|
|
_inherit = ['mail.thread']
|
|
|
|
name = fields.Char(string='Name', required=True, tracking=True)
|
|
rule_type = fields.Selection(
|
|
selection=[
|
|
('match', 'Match'),
|
|
('classify', 'Classify'),
|
|
('audit', 'Audit'),
|
|
('fee', 'Fee'),
|
|
('routing', 'Routing'),
|
|
('followup', 'Follow-Up'),
|
|
],
|
|
string='Type',
|
|
required=True,
|
|
tracking=True,
|
|
)
|
|
description = fields.Text(
|
|
string='Description',
|
|
help='Natural language description read by the AI.',
|
|
)
|
|
trigger_domain = fields.Text(
|
|
string='Trigger Domain (JSON)',
|
|
help='Odoo domain filter for matching records.',
|
|
)
|
|
match_logic = fields.Text(
|
|
string='Match Logic',
|
|
help='Natural language matching instructions for the AI.',
|
|
)
|
|
match_code = fields.Text(
|
|
string='Match Code (Python)',
|
|
help='Optional deterministic Python matching code.',
|
|
)
|
|
fee_account_id = fields.Many2one(
|
|
'account.account', string='Fee Account',
|
|
)
|
|
write_off_account_id = fields.Many2one(
|
|
'account.account', string='Write-Off Account',
|
|
)
|
|
approval_tier = fields.Selection(
|
|
selection=[('auto', 'Auto-Approved'), ('needs_approval', 'Needs Approval')],
|
|
string='Approval Tier',
|
|
default='needs_approval',
|
|
tracking=True,
|
|
)
|
|
created_by = fields.Selection(
|
|
selection=[('admin', 'Admin'), ('ai', 'AI')],
|
|
string='Created By',
|
|
default='admin',
|
|
)
|
|
confidence_score = fields.Float(
|
|
string='Confidence Score', digits=(3, 2), default=0.0,
|
|
)
|
|
total_uses = fields.Integer(string='Total Uses', default=0)
|
|
total_approved = fields.Integer(string='Total Approved', default=0)
|
|
total_rejected = fields.Integer(string='Total Rejected', default=0)
|
|
promotion_threshold = fields.Float(
|
|
string='Promotion Threshold', default=0.95,
|
|
)
|
|
min_sample_size = fields.Integer(string='Min Sample Size', default=30)
|
|
active = fields.Boolean(string='Active', default=True, tracking=True)
|
|
version = fields.Integer(string='Version', default=1)
|
|
parent_rule_id = fields.Many2one(
|
|
'fusion.accounting.rule', string='Previous Version',
|
|
ondelete='set null',
|
|
)
|
|
journal_ids = fields.Many2many(
|
|
'account.journal', string='Journals',
|
|
)
|
|
company_id = fields.Many2one(
|
|
'res.company', string='Company',
|
|
default=lambda self: self.env.company,
|
|
)
|
|
sequence = fields.Integer(string='Sequence', default=10)
|
|
notes = fields.Text(string='Notes')
|
|
|
|
def _record_decision(self, approved=True):
|
|
for rec in self:
|
|
self.env.cr.execute("""
|
|
UPDATE fusion_accounting_rule
|
|
SET total_uses = total_uses + 1,
|
|
total_approved = total_approved + %s,
|
|
total_rejected = total_rejected + %s
|
|
WHERE id = %s
|
|
RETURNING total_uses, total_approved
|
|
""", (int(approved), int(not approved), rec.id))
|
|
row = self.env.cr.fetchone()
|
|
rec.invalidate_recordset(['total_uses', 'total_approved', 'total_rejected'])
|
|
if row and row[0] > 0:
|
|
rec.confidence_score = row[1] / row[0]
|
|
rec._check_promotion()
|
|
|
|
def _check_promotion(self):
|
|
for rec in self:
|
|
if (rec.approval_tier == 'needs_approval'
|
|
and rec.total_uses >= rec.min_sample_size
|
|
and rec.confidence_score >= rec.promotion_threshold):
|
|
rec.approval_tier = 'auto'
|
|
_logger.info(
|
|
"Rule '%s' promoted to auto-approved (confidence=%.2f, uses=%d)",
|
|
rec.name, rec.confidence_score, rec.total_uses,
|
|
)
|
|
|
|
def action_demote(self):
|
|
self.write({'approval_tier': 'needs_approval'})
|
|
|
|
def action_rollback(self):
|
|
for rec in self:
|
|
if rec.parent_rule_id:
|
|
rec.active = False
|
|
rec.parent_rule_id.active = True
|