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