Files
Odoo-Modules/fusion_accounting/models/accounting_dashboard.py
gsinghpal 4cd7357aa0 changes
2026-04-02 23:40:34 -04:00

279 lines
11 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
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',
}