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

221 lines
7.3 KiB
Python

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_id', '=', 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_id', '=', 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,
}