feat(fusion_accounting_followup): fusion.followup.run audit model
Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting Follow-up',
|
'name': 'Fusion Accounting Follow-up',
|
||||||
'version': '19.0.1.0.6',
|
'version': '19.0.1.0.7',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
from . import fusion_followup_level
|
from . import fusion_followup_level
|
||||||
|
from . import fusion_followup_run
|
||||||
|
|||||||
54
fusion_accounting_followup/models/fusion_followup_run.py
Normal file
54
fusion_accounting_followup/models/fusion_followup_run.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""Audit record of one follow-up execution (per partner per level)."""
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
STATE_SELECTION = [
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('sent', 'Sent'),
|
||||||
|
('manual_review', 'Manual Review'),
|
||||||
|
('skipped', 'Skipped'),
|
||||||
|
('failed', 'Failed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FusionFollowupRun(models.Model):
|
||||||
|
_name = "fusion.followup.run"
|
||||||
|
_description = "Fusion Follow-up Run (Per-Partner Audit)"
|
||||||
|
_order = "execution_date desc, id desc"
|
||||||
|
|
||||||
|
partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
|
||||||
|
company_id = fields.Many2one('res.company', required=True,
|
||||||
|
default=lambda self: self.env.company)
|
||||||
|
level_id = fields.Many2one('fusion.followup.level', ondelete='restrict')
|
||||||
|
|
||||||
|
execution_date = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||||
|
state = fields.Selection(STATE_SELECTION, default='draft', required=True)
|
||||||
|
|
||||||
|
overdue_amount = fields.Float()
|
||||||
|
longest_overdue_days = fields.Integer()
|
||||||
|
|
||||||
|
risk_score = fields.Integer()
|
||||||
|
risk_band = fields.Selection([
|
||||||
|
('low', 'Low'), ('medium', 'Medium'),
|
||||||
|
('high', 'High'), ('critical', 'Critical'),
|
||||||
|
])
|
||||||
|
|
||||||
|
subject = fields.Char()
|
||||||
|
body = fields.Text()
|
||||||
|
tone_used = fields.Selection([
|
||||||
|
('gentle', 'Gentle'), ('firm', 'Firm'), ('legal', 'Legal'),
|
||||||
|
])
|
||||||
|
sent_to_email = fields.Char()
|
||||||
|
|
||||||
|
text_was_ai_generated = fields.Boolean(default=False)
|
||||||
|
ai_provider = fields.Char(help="LLM provider name (openai, claude, etc.) if AI was used.")
|
||||||
|
|
||||||
|
notes = fields.Text()
|
||||||
|
error_message = fields.Text()
|
||||||
|
|
||||||
|
def action_mark_sent(self):
|
||||||
|
self.write({'state': 'sent'})
|
||||||
|
|
||||||
|
def action_mark_failed(self, error: str = ''):
|
||||||
|
self.write({'state': 'failed', 'error_message': error})
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_fusion_followup_level_user,fusion.followup.level.user,model_fusion_followup_level,base.group_user,1,0,0,0
|
access_fusion_followup_level_user,fusion.followup.level.user,model_fusion_followup_level,base.group_user,1,0,0,0
|
||||||
access_fusion_followup_level_admin,fusion.followup.level.admin,model_fusion_followup_level,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
access_fusion_followup_level_admin,fusion.followup.level.admin,model_fusion_followup_level,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||||
|
access_fusion_followup_run_user,fusion.followup.run.user,model_fusion_followup_run,base.group_user,1,0,0,0
|
||||||
|
access_fusion_followup_run_admin,fusion.followup.run.admin,model_fusion_followup_run,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||||
|
|||||||
|
@@ -4,3 +4,4 @@ from . import test_risk_scorer
|
|||||||
from . import test_tone_selector
|
from . import test_tone_selector
|
||||||
from . import test_followup_text_generator
|
from . import test_followup_text_generator
|
||||||
from . import test_fusion_followup_level
|
from . import test_fusion_followup_level
|
||||||
|
from . import test_fusion_followup_run
|
||||||
|
|||||||
44
fusion_accounting_followup/tests/test_fusion_followup_run.py
Normal file
44
fusion_accounting_followup/tests/test_fusion_followup_run.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class TestFusionFollowupRun(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.partner = cls.env['res.partner'].create({'name': 'Run Test Partner'})
|
||||||
|
cls.level = cls.env['fusion.followup.level'].create({
|
||||||
|
'name': 'Reminder', 'sequence': 301, 'delay_days': 7, 'tone': 'gentle',
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_create_minimal(self):
|
||||||
|
run = self.env['fusion.followup.run'].create({
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'level_id': self.level.id,
|
||||||
|
})
|
||||||
|
self.assertEqual(run.state, 'draft')
|
||||||
|
self.assertTrue(run.execution_date)
|
||||||
|
|
||||||
|
def test_action_mark_sent(self):
|
||||||
|
run = self.env['fusion.followup.run'].create({
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'level_id': self.level.id,
|
||||||
|
})
|
||||||
|
run.action_mark_sent()
|
||||||
|
self.assertEqual(run.state, 'sent')
|
||||||
|
|
||||||
|
def test_action_mark_failed_records_error(self):
|
||||||
|
run = self.env['fusion.followup.run'].create({
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
})
|
||||||
|
run.action_mark_failed(error='SMTP unreachable')
|
||||||
|
self.assertEqual(run.state, 'failed')
|
||||||
|
self.assertEqual(run.error_message, 'SMTP unreachable')
|
||||||
|
|
||||||
|
def test_partner_required(self):
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
self.env['fusion.followup.run'].create({
|
||||||
|
'level_id': self.level.id,
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user