Files
Odoo-Modules/fusion_accounting/models/accounting_dashboard.py
gsinghpal 3cc93b8783 changes
2026-04-04 15:37:16 -04:00

335 lines
14 KiB
Python

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
# M4: Guard against made_sequence_gap field not existing
try:
gaps = self.env['account.move'].search_count([
('state', '=', 'posted'),
('company_id', '=', rec.company_id.id),
('made_sequence_gap', '=', True),
])
except (ValueError, KeyError):
gaps = 0
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 = []
today = fields.Date.today()
# Pending AI approvals (highest priority)
pending = self.env['fusion.accounting.match.history'].search_count([
('decision', '=', 'pending'),
('company_id', '=', rec.company_id.id),
])
if pending > 0:
attention.append({
'priority': 0, 'severity': 'danger',
'title': f'{pending} AI actions awaiting your approval',
'domain': 'audit',
'action': 'Review and approve or reject pending actions',
'prompt': 'Show me all pending approval actions',
})
# Unreconciled bank lines
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, 'severity': 'warning',
'title': f'{unrecon} unreconciled bank lines',
'domain': 'bank_reconciliation',
'action': 'Review and reconcile bank statement lines',
'prompt': 'Show me unreconciled bank lines across all journals with a breakdown by journal',
})
# Overdue customer invoices
overdue = self.env['account.move'].search_count([
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ('not_paid', 'partial')),
('invoice_date_due', '<', today),
('company_id', '=', rec.company_id.id),
])
if overdue > 0:
attention.append({
'priority': 2, 'severity': 'warning',
'title': f'{overdue} overdue customer invoices',
'domain': 'accounts_receivable',
'action': 'Send follow-up reminders',
'prompt': 'Show me overdue invoices sorted by amount',
})
# Unpaid vendor bills due this week
week_end = today + timedelta(days=7)
due_bills = self.env['account.move'].search_count([
('move_type', '=', 'in_invoice'),
('state', '=', 'posted'),
('payment_state', 'in', ('not_paid', 'partial')),
('invoice_date_due', '<=', week_end),
('invoice_date_due', '>=', today),
('company_id', '=', rec.company_id.id),
])
if due_bills > 0:
attention.append({
'priority': 3, 'severity': 'info',
'title': f'{due_bills} vendor bills due this week',
'domain': 'accounts_payable',
'action': 'Review upcoming payments',
'prompt': f'Show me vendor bills due between {today} and {week_end}',
})
# Stale draft entries
drafts = self.env['account.move'].search_count([
('state', '=', 'draft'),
('date', '<=', today - timedelta(days=30)),
('company_id', '=', rec.company_id.id),
])
if drafts > 0:
attention.append({
'priority': 4, 'severity': 'muted',
'title': f'{drafts} stale draft entries (30+ days)',
'domain': 'journal_review',
'action': 'Post or delete stale draft entries',
'prompt': 'Find all stale draft entries older than 30 days',
})
# Unmatched customer payments (on outstanding receipts accounts)
try:
outstanding_accts = self.env['account.account'].search([
('name', 'ilike', 'outstanding receipt'),
('company_ids', 'in', rec.company_id.id),
])
if outstanding_accts:
unmatched_payments = self.env['account.move.line'].search_count([
('account_id', 'in', outstanding_accts.ids),
('parent_state', '=', 'posted'),
('reconciled', '=', False),
('company_id', '=', rec.company_id.id),
])
if unmatched_payments > 0:
attention.append({
'priority': 5, 'severity': 'info',
'title': f'{unmatched_payments} unmatched customer payments',
'domain': 'accounts_receivable',
'action': 'Match payments to invoices',
'prompt': 'Show me unmatched customer payments that need to be applied to invoices',
})
except Exception:
pass
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': r.proposed_at.isoformat() if r.proposed_at else '',
'amount': r.amount,
} for r in recent])
def action_refresh(self):
return {
'type': 'ir.actions.client',
'tag': 'fusion_accounting.dashboard',
}