import logging from odoo import fields _logger = logging.getLogger(__name__) ACCOUNT_TYPE_EXPECTED_DIRECTION = { 'asset_receivable': 'debit', 'asset_cash': 'debit', 'asset_current': 'debit', 'asset_non_current': 'debit', 'asset_prepayments': 'debit', 'asset_fixed': 'debit', 'liability_payable': 'credit', 'liability_credit_card': 'credit', 'liability_current': 'credit', 'liability_non_current': 'credit', 'equity': 'credit', 'equity_unaffected': 'credit', 'income': 'credit', 'income_other': 'credit', 'expense': 'debit', 'expense_depreciation': 'debit', 'expense_direct_cost': 'debit', 'off_balance': None, } def find_wrong_direction_balances(env, params): balance_data = env['account.move.line'].read_group( [('parent_state', '=', 'posted'), ('company_id', '=', env.company.id)], ['balance:sum'], ['account_id'], ) acct_ids = [row['account_id'][0] for row in balance_data if row.get('account_id')] acct_map = {} if acct_ids: for acct in env['account.account'].browse(acct_ids): acct_map[acct.id] = acct issues = [] for row in balance_data: if not row.get('account_id'): continue acct = acct_map.get(row['account_id'][0]) if not acct: continue expected = ACCOUNT_TYPE_EXPECTED_DIRECTION.get(acct.account_type) if not expected: continue balance = row.get('balance', 0) or 0 if (expected == 'debit' and balance < -0.01) or (expected == 'credit' and balance > 0.01): issues.append({ 'account_id': acct.id, 'code': acct.code, 'name': acct.name, 'balance': balance, 'expected': expected, 'actual': 'credit' if balance < 0 else 'debit', }) return {'count': len(issues), 'issues': issues} def find_duplicate_entries(env, params): date_from = params.get('date_from') date_to = params.get('date_to') domain = [ ('state', '=', 'posted'), ('company_id', '=', env.company.id), ] if date_from: domain.append(('date', '>=', date_from)) if date_to: domain.append(('date', '<=', date_to)) moves = env['account.move'].search(domain, order='partner_id, amount_total, date') duplicates = [] prev = None for move in moves: if prev and ( prev.partner_id == move.partner_id and prev.partner_id and abs(prev.amount_total - move.amount_total) < 0.01 and prev.date == move.date and prev.journal_id == move.journal_id ): duplicates.append({ 'entry_1': {'id': prev.id, 'name': prev.name}, 'entry_2': {'id': move.id, 'name': move.name}, 'partner': move.partner_id.name, 'amount': move.amount_total, 'date': str(move.date), }) prev = move return {'count': len(duplicates), 'duplicates': duplicates[:20]} def find_wrong_account_entries(env, params): date_from = params.get('date_from') date_to = params.get('date_to') domain = [ ('parent_state', '=', 'posted'), ('company_id', '=', env.company.id), ] if date_from: domain.append(('date', '>=', date_from)) if date_to: domain.append(('date', '<=', date_to)) issues = [] tax_accounts = env['account.account'].search([ ('account_type', 'in', ('liability_current', 'asset_current')), ('code', '=like', '2005%'), ('company_ids', 'in', env.company.id), ]) if tax_accounts: revenue_on_tax = env['account.move.line'].search( domain + [ ('account_id', 'in', tax_accounts.ids), ('product_id', '!=', False), ] ) for line in revenue_on_tax[:20]: issues.append({ 'id': line.id, 'move': line.move_id.name, 'account': f'{line.account_id.code} {line.account_id.name}', 'product': line.product_id.name, 'amount': line.balance, 'issue': 'Product line on tax account', }) return {'count': len(issues), 'issues': issues} def find_sequence_gaps(env, params): moves = env['account.move'].search([ ('state', '=', 'posted'), ('company_id', '=', env.company.id), ('made_sequence_gap', '=', True), ], order='date desc', limit=50) return { 'count': len(moves), 'gaps': [{ 'id': m.id, 'name': m.name, 'date': str(m.date), 'journal': m.journal_id.name, } for m in moves], } def find_draft_entries(env, params): min_age_days = int(params.get('min_age_days', 30)) from datetime import timedelta cutoff = fields.Date.today() - timedelta(days=min_age_days) drafts = env['account.move'].search([ ('state', '=', 'draft'), ('date', '<=', cutoff), ('company_id', '=', env.company.id), ], order='date asc', limit=50) return { 'count': len(drafts), 'entries': [{ 'id': d.id, 'name': d.name or 'Draft', 'date': str(d.date), 'journal': d.journal_id.name, 'amount': d.amount_total, 'partner': d.partner_id.name if d.partner_id else '', } for d in drafts], } def find_unreconciled_suspense(env, params): suspense_accounts = env['account.account'].search([ ('code', '=like', '999%'), ('company_ids', 'in', env.company.id), ]) issues = [] for acct in suspense_accounts: balance = sum(env['account.move.line'].search([ ('account_id', '=', acct.id), ('parent_state', '=', 'posted'), ]).mapped('balance')) if abs(balance) > 0.01: issues.append({ 'account_id': acct.id, 'code': acct.code, 'name': acct.name, 'balance': balance, }) return {'count': len(issues), 'accounts': issues} def verify_reconciliation_integrity(env, params): partials = env['account.partial.reconcile'].search([ ('company_id', '=', env.company.id), ], limit=500) issues = [] for p in partials: debit_ok = p.debit_move_id.reconciled or abs(p.debit_move_id.amount_residual) < 0.01 credit_ok = p.credit_move_id.reconciled or abs(p.credit_move_id.amount_residual) < 0.01 if not debit_ok and not credit_ok: issues.append({ 'id': p.id, 'debit_move': p.debit_move_id.move_id.name, 'credit_move': p.credit_move_id.move_id.name, 'amount': p.amount, 'debit_residual': p.debit_move_id.amount_residual, 'credit_residual': p.credit_move_id.amount_residual, }) return {'count': len(issues), 'issues': issues[:20]} TOOLS = { 'find_wrong_direction_balances': find_wrong_direction_balances, 'find_duplicate_entries': find_duplicate_entries, 'find_wrong_account_entries': find_wrong_account_entries, 'find_sequence_gaps': find_sequence_gaps, 'find_draft_entries': find_draft_entries, 'find_unreconciled_suspense': find_unreconciled_suspense, 'verify_reconciliation_integrity': verify_reconciliation_integrity, }