git mv preserves history. fusion_accounting/ retains only __manifest__.py, __init__.py, CLAUDE.md, and docs/ — the meta-module shell. All Python, data, views, security, services, static, tests, wizards, report move to fusion_accounting_ai/. Manifest data list updated; security.xml move to _core deferred to Task 12. Made-with: Cursor
221 lines
7.3 KiB
Python
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_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,
|
|
}
|