diff --git a/fusion_accounting_followup/__manifest__.py b/fusion_accounting_followup/__manifest__.py index c2336df3..316a3177 100644 --- a/fusion_accounting_followup/__manifest__.py +++ b/fusion_accounting_followup/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Follow-up', - 'version': '19.0.1.0.5', + 'version': '19.0.1.0.6', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented customer follow-ups (dunning) for unpaid invoices.', 'description': """ diff --git a/fusion_accounting_followup/models/__init__.py b/fusion_accounting_followup/models/__init__.py index e69de29b..c7fb1f45 100644 --- a/fusion_accounting_followup/models/__init__.py +++ b/fusion_accounting_followup/models/__init__.py @@ -0,0 +1 @@ +from . import fusion_followup_level diff --git a/fusion_accounting_followup/models/fusion_followup_level.py b/fusion_accounting_followup/models/fusion_followup_level.py new file mode 100644 index 00000000..e2e5d9d2 --- /dev/null +++ b/fusion_accounting_followup/models/fusion_followup_level.py @@ -0,0 +1,42 @@ +"""Follow-up level definition (e.g. Reminder at 7 days, Warning at 30, Legal at 60).""" + +from odoo import _, api, fields, models + + +TONE_SELECTION = [ + ('gentle', 'Gentle'), + ('firm', 'Firm'), + ('legal', 'Legal'), +] + + +class FusionFollowupLevel(models.Model): + _name = "fusion.followup.level" + _description = "Fusion Follow-up Level" + _order = "sequence, id" + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(required=True, default=10, + help="Order in which levels escalate (1, 2, 3...).") + delay_days = fields.Integer(required=True, + help="Min days overdue to trigger this level.") + tone = fields.Selection(TONE_SELECTION, required=True, default='gentle') + description = fields.Text() + company_id = fields.Many2one('res.company', default=lambda self: self.env.company) + + mail_template_id = fields.Many2one('mail.template', + domain=[('model', '=', 'res.partner')]) + + requires_manual_review = fields.Boolean(default=False, + help="If True, follow-ups at this level need human approval before send.") + + active = fields.Boolean(default=True) + + _check_delay_positive = models.Constraint( + 'CHECK(delay_days >= 0)', + 'delay_days must be non-negative.', + ) + _unique_sequence_per_company = models.Constraint( + 'UNIQUE(company_id, sequence)', + 'Sequence must be unique per company.', + ) diff --git a/fusion_accounting_followup/security/ir.model.access.csv b/fusion_accounting_followup/security/ir.model.access.csv index 97dd8b91..ccb4588c 100644 --- a/fusion_accounting_followup/security/ir.model.access.csv +++ b/fusion_accounting_followup/security/ir.model.access.csv @@ -1 +1,3 @@ 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_admin,fusion.followup.level.admin,model_fusion_followup_level,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1 diff --git a/fusion_accounting_followup/tests/__init__.py b/fusion_accounting_followup/tests/__init__.py index 68af990b..67265c7b 100644 --- a/fusion_accounting_followup/tests/__init__.py +++ b/fusion_accounting_followup/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_level_resolver from . import test_risk_scorer from . import test_tone_selector from . import test_followup_text_generator +from . import test_fusion_followup_level diff --git a/fusion_accounting_followup/tests/test_fusion_followup_level.py b/fusion_accounting_followup/tests/test_fusion_followup_level.py new file mode 100644 index 00000000..1bb0bcc3 --- /dev/null +++ b/fusion_accounting_followup/tests/test_fusion_followup_level.py @@ -0,0 +1,42 @@ +from odoo.tests.common import TransactionCase +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestFusionFollowupLevel(TransactionCase): + + def test_create_minimal(self): + level = self.env['fusion.followup.level'].create({ + 'name': 'Reminder', 'sequence': 1, 'delay_days': 7, 'tone': 'gentle', + }) + self.assertEqual(level.name, 'Reminder') + self.assertTrue(level.active) + + def test_negative_delay_rejected(self): + with self.assertRaises(Exception): + self.env['fusion.followup.level'].create({ + 'name': 'Bad', 'sequence': 1, 'delay_days': -5, 'tone': 'gentle', + }) + + def test_duplicate_sequence_rejected(self): + self.env['fusion.followup.level'].create({ + 'name': 'A', 'sequence': 100, 'delay_days': 7, 'tone': 'gentle', + }) + with self.assertRaises(Exception): + self.env['fusion.followup.level'].create({ + 'name': 'B', 'sequence': 100, 'delay_days': 30, 'tone': 'firm', + }) + + def test_three_levels_escalate(self): + for seq, name, days, tone in [(1, 'R', 7, 'gentle'), + (2, 'W', 30, 'firm'), + (3, 'L', 60, 'legal')]: + self.env['fusion.followup.level'].create({ + 'name': name, 'sequence': seq + 200, + 'delay_days': days, 'tone': tone, + }) + levels = self.env['fusion.followup.level'].search([ + ('sequence', '>', 200), + ], order='sequence') + self.assertEqual(len(levels), 3) + self.assertEqual(levels.mapped('tone'), ['gentle', 'firm', 'legal'])