import json import logging from datetime import timedelta from odoo import models, fields, api _logger = logging.getLogger(__name__) class FusionAccountingDashboard(models.TransientModel): _name = 'fusion.accounting.dashboard' _description = 'Fusion Accounting Dashboard' company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env.company, ) bank_recon_count = fields.Integer(compute='_compute_bank_recon') bank_recon_amount = fields.Monetary( compute='_compute_bank_recon', currency_field='currency_id', ) ar_total = fields.Monetary( compute='_compute_ar', currency_field='currency_id', ) ar_overdue_count = fields.Integer(compute='_compute_ar') ap_total = fields.Monetary( compute='_compute_ap', currency_field='currency_id', ) ap_due_this_week = fields.Integer(compute='_compute_ap') hst_balance = fields.Monetary( compute='_compute_hst', currency_field='currency_id', ) audit_score = fields.Integer(compute='_compute_audit') audit_flag_count = fields.Integer(compute='_compute_audit') month_end_status = fields.Char(compute='_compute_month_end') month_end_open_items = fields.Integer(compute='_compute_month_end') currency_id = fields.Many2one( 'res.currency', string='Currency', default=lambda self: self.env.company.currency_id, ) needs_attention_json = fields.Text(compute='_compute_action_centre') recent_activity_json = fields.Text(compute='_compute_action_centre') @api.depends('company_id') def _compute_bank_recon(self): for rec in self: data = self.env['account.bank.statement.line'].read_group( [('is_reconciled', '=', False), ('company_id', '=', rec.company_id.id)], ['amount:sum'], [], ) row = data[0] if data else {} rec.bank_recon_count = row.get('__count', 0) rec.bank_recon_amount = abs(row.get('amount', 0) or 0) @api.depends('company_id') def _compute_ar(self): for rec in self: data = self.env['account.move.line'].read_group( [ ('account_id.account_type', '=', 'asset_receivable'), ('parent_state', '=', 'posted'), ('reconciled', '=', False), ('company_id', '=', rec.company_id.id), ], ['amount_residual:sum'], [], ) row = data[0] if data else {} rec.ar_total = row.get('amount_residual', 0) or 0 rec.ar_overdue_count = self.env['account.move.line'].search_count([ ('account_id.account_type', '=', 'asset_receivable'), ('parent_state', '=', 'posted'), ('reconciled', '=', False), ('date_maturity', '<', fields.Date.today()), ('company_id', '=', rec.company_id.id), ]) @api.depends('company_id') def _compute_ap(self): for rec in self: data = self.env['account.move.line'].read_group( [ ('account_id.account_type', '=', 'liability_payable'), ('parent_state', '=', 'posted'), ('reconciled', '=', False), ('company_id', '=', rec.company_id.id), ], ['amount_residual:sum'], [], ) row = data[0] if data else {} rec.ap_total = abs(row.get('amount_residual', 0) or 0) week_end = fields.Date.today() + timedelta(days=7) rec.ap_due_this_week = self.env['account.move.line'].search_count([ ('account_id.account_type', '=', 'liability_payable'), ('parent_state', '=', 'posted'), ('reconciled', '=', False), ('date_maturity', '<=', week_end), ('company_id', '=', rec.company_id.id), ]) @api.depends('company_id') def _compute_hst(self): for rec in self: collected_data = self.env['account.move.line'].read_group( [ ('account_id.code', '=like', '2005%'), ('parent_state', '=', 'posted'), ('company_id', '=', rec.company_id.id), ], ['balance:sum'], [], ) itc_data = self.env['account.move.line'].read_group( [ ('account_id.code', '=like', '2006%'), ('parent_state', '=', 'posted'), ('company_id', '=', rec.company_id.id), ], ['balance:sum'], [], ) collected = abs((collected_data[0] if collected_data else {}).get('balance', 0) or 0) itcs = abs((itc_data[0] if itc_data else {}).get('balance', 0) or 0) rec.hst_balance = collected - itcs @api.depends('company_id') def _compute_audit(self): for rec in self: issues = 0 # Wrong-direction balances via read_group balance_data = self.env['account.move.line'].read_group( [('parent_state', '=', 'posted'), ('company_id', '=', rec.company_id.id)], ['balance:sum'], ['account_id'], ) acct_cache = {} acct_ids = [row['account_id'][0] for row in balance_data if row.get('account_id')] if acct_ids: for acct in self.env['account.account'].browse(acct_ids): acct_cache[acct.id] = acct.account_type for row in balance_data: if not row.get('account_id'): continue acct_type = acct_cache.get(row['account_id'][0], '') balance = row.get('balance', 0) or 0 if acct_type in ('asset_receivable', 'asset_cash', 'asset_current', 'asset_non_current', 'asset_fixed', 'expense', 'expense_depreciation', 'expense_direct_cost'): if balance < -0.01: issues += 1 elif acct_type in ('liability_payable', 'liability_current', 'liability_non_current', 'equity', 'income', 'income_other'): if balance > 0.01: issues += 1 gaps = self.env['account.move'].search_count([ ('state', '=', 'posted'), ('company_id', '=', rec.company_id.id), ('made_sequence_gap', '=', True), ]) issues += gaps pending_approvals = self.env['fusion.accounting.match.history'].search_count([ ('decision', '=', 'pending'), ('company_id', '=', rec.company_id.id), ]) rec.audit_score = max(0, min(100, 100 - issues * 3)) rec.audit_flag_count = issues + pending_approvals @api.depends('company_id') def _compute_month_end(self): for rec in self: open_items = 0 open_items += self.env['account.bank.statement.line'].search_count([ ('is_reconciled', '=', False), ('company_id', '=', rec.company_id.id), ]) open_items += self.env['account.move'].search_count([ ('state', '=', 'draft'), ('company_id', '=', rec.company_id.id), ]) suspense_data = self.env['account.move.line'].read_group( [ ('account_id.code', '=like', '999%'), ('parent_state', '=', 'posted'), ('company_id', '=', rec.company_id.id), ], ['balance:sum'], ['account_id'], ) for row in suspense_data: if abs(row.get('balance', 0) or 0) > 0.01: open_items += 1 rec.month_end_open_items = open_items if open_items == 0: rec.month_end_status = 'Ready to Close' elif open_items < 5: rec.month_end_status = 'Almost Ready' else: rec.month_end_status = 'Open' @api.depends('company_id') def _compute_action_centre(self): for rec in self: attention = [] unrecon = self.env['account.bank.statement.line'].search_count([ ('is_reconciled', '=', False), ('company_id', '=', rec.company_id.id), ]) if unrecon > 0: attention.append({ 'priority': 1, 'title': f'{unrecon} unreconciled bank lines', 'domain': 'bank_reconciliation', 'action': 'Review and reconcile bank statement lines', }) overdue = self.env['account.move'].search_count([ ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('payment_state', 'in', ('not_paid', 'partial')), ('invoice_date_due', '<', fields.Date.today()), ('company_id', '=', rec.company_id.id), ]) if overdue > 0: attention.append({ 'priority': 2, 'title': f'{overdue} overdue customer invoices', 'domain': 'accounts_receivable', 'action': 'Send follow-up reminders', }) pending = self.env['fusion.accounting.match.history'].search_count([ ('decision', '=', 'pending'), ('company_id', '=', rec.company_id.id), ]) if pending > 0: attention.append({ 'priority': 0, 'title': f'{pending} AI actions awaiting approval', 'domain': 'audit', 'action': 'Review and approve/reject pending actions', }) drafts = self.env['account.move'].search_count([ ('state', '=', 'draft'), ('date', '<=', fields.Date.today() - timedelta(days=30)), ('company_id', '=', rec.company_id.id), ]) if drafts > 0: attention.append({ 'priority': 3, 'title': f'{drafts} stale draft entries (30+ days)', 'domain': 'journal_review', 'action': 'Post or delete stale draft entries', }) attention.sort(key=lambda x: x['priority']) rec.needs_attention_json = json.dumps(attention) recent = self.env['fusion.accounting.match.history'].search([ ('company_id', '=', rec.company_id.id), ], limit=10, order='proposed_at desc') rec.recent_activity_json = json.dumps([{ 'tool': r.tool_name, 'decision': r.decision, 'date': str(r.proposed_at), 'amount': r.amount, } for r in recent]) def action_refresh(self): return { 'type': 'ir.actions.client', 'tag': 'fusion_accounting.dashboard', }