changes
This commit is contained in:
278
fusion_accounting/models/accounting_dashboard.py
Normal file
278
fusion_accounting/models/accounting_dashboard.py
Normal file
@@ -0,0 +1,278 @@
|
||||
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',
|
||||
}
|
||||
Reference in New Issue
Block a user