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
257 lines
8.8 KiB
Python
257 lines
8.8 KiB
Python
import logging
|
|
from odoo import fields
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_payroll_entries(env, params):
|
|
payroll_journals = env['account.journal'].search([
|
|
('name', 'ilike', 'payroll'),
|
|
('company_id', '=', env.company.id),
|
|
])
|
|
if not payroll_journals and params.get('journal_id'):
|
|
payroll_journals = env['account.journal'].browse(int(params['journal_id']))
|
|
domain = [
|
|
('journal_id', 'in', payroll_journals.ids),
|
|
('state', '=', 'posted'),
|
|
('company_id', '=', env.company.id),
|
|
]
|
|
if params.get('date_from'):
|
|
domain.append(('date', '>=', params['date_from']))
|
|
if params.get('date_to'):
|
|
domain.append(('date', '<=', params['date_to']))
|
|
entries = env['account.move'].search(domain, order='date desc', limit=50)
|
|
return {
|
|
'count': len(entries),
|
|
'entries': [{
|
|
'id': e.id, 'name': e.name, 'date': str(e.date),
|
|
'amount': e.amount_total, 'ref': e.ref or '',
|
|
} for e in entries],
|
|
}
|
|
|
|
|
|
def compare_payroll_to_bank(env, params):
|
|
date_from = params.get('date_from')
|
|
date_to = params.get('date_to')
|
|
if not date_from or not date_to:
|
|
return {'error': 'date_from and date_to are required'}
|
|
payroll_journals = env['account.journal'].search([
|
|
('name', 'ilike', 'payroll'), ('company_id', '=', env.company.id),
|
|
])
|
|
payroll_entries = env['account.move'].search([
|
|
('journal_id', 'in', payroll_journals.ids),
|
|
('state', '=', 'posted'),
|
|
('date', '>=', date_from), ('date', '<=', date_to),
|
|
])
|
|
bank_lines = env['account.bank.statement.line'].search([
|
|
('date', '>=', date_from), ('date', '<=', date_to),
|
|
('company_id', '=', env.company.id),
|
|
])
|
|
payroll_total = sum(e.amount_total for e in payroll_entries)
|
|
bank_payroll = sum(abs(l.amount) for l in bank_lines if 'payroll' in (l.payment_ref or '').lower())
|
|
return {
|
|
'payroll_journal_total': payroll_total,
|
|
'bank_payroll_total': bank_payroll,
|
|
'difference': payroll_total - bank_payroll,
|
|
}
|
|
|
|
|
|
def verify_source_deductions(env, params):
|
|
return {
|
|
'status': 'info',
|
|
'message': 'Source deduction verification requires CRA rate tables. Use fusion_payroll for full verification.',
|
|
}
|
|
|
|
|
|
def get_cra_remittance_status(env, params):
|
|
cra_accounts = env['account.account'].search([
|
|
('name', 'ilike', 'CRA'),
|
|
('company_ids', 'in', env.company.id),
|
|
])
|
|
result = []
|
|
for acct in cra_accounts:
|
|
balance = sum(env['account.move.line'].search([
|
|
('account_id', '=', acct.id),
|
|
('parent_state', '=', 'posted'),
|
|
]).mapped('balance'))
|
|
result.append({'code': acct.code, 'name': acct.name, 'balance': balance})
|
|
return {'accounts': result}
|
|
|
|
|
|
def find_unmatched_payroll_cheques(env, params):
|
|
bank_lines = env['account.bank.statement.line'].search([
|
|
('is_reconciled', '=', False),
|
|
('company_id', '=', env.company.id),
|
|
('payment_ref', 'ilike', 'cheque'),
|
|
])
|
|
return {
|
|
'count': len(bank_lines),
|
|
'cheques': [{
|
|
'id': l.id, 'date': str(l.date),
|
|
'ref': l.payment_ref, 'amount': l.amount,
|
|
} for l in bank_lines[:30]],
|
|
}
|
|
|
|
|
|
def parse_payroll_summary(env, params):
|
|
import re
|
|
raw_data = params.get('data', '')
|
|
if not raw_data:
|
|
return {'error': 'No payroll data provided'}
|
|
|
|
lines = raw_data.strip().split('\n')
|
|
entries = []
|
|
totals = {'gross': 0, 'cpp': 0, 'ei': 0, 'tax': 0, 'net': 0}
|
|
|
|
for line in lines:
|
|
amounts = re.findall(r'\$?([\d,]+\.?\d*)', line)
|
|
if len(amounts) >= 2:
|
|
name_part = re.sub(r'\$?[\d,]+\.?\d*', '', line).strip(' \t,|-')
|
|
parsed_amounts = [float(a.replace(',', '')) for a in amounts]
|
|
entry = {'name': name_part or 'Employee', 'amounts': parsed_amounts}
|
|
if len(parsed_amounts) >= 5:
|
|
entry.update({
|
|
'gross': parsed_amounts[0],
|
|
'cpp': parsed_amounts[1],
|
|
'ei': parsed_amounts[2],
|
|
'tax': parsed_amounts[3],
|
|
'net': parsed_amounts[4] if len(parsed_amounts) > 4 else parsed_amounts[0] - sum(parsed_amounts[1:4]),
|
|
})
|
|
for k in ('gross', 'cpp', 'ei', 'tax', 'net'):
|
|
totals[k] += entry.get(k, 0)
|
|
entries.append(entry)
|
|
|
|
return {
|
|
'status': 'parsed',
|
|
'employee_count': len(entries),
|
|
'entries': entries,
|
|
'totals': totals,
|
|
'raw_lines': len(lines),
|
|
}
|
|
|
|
|
|
def _resolve_account_id(env, val):
|
|
"""Resolve an account code or ID to a valid account ID.
|
|
Accepts: integer ID, string ID, or account code string like '2201'."""
|
|
if not val:
|
|
return False
|
|
val_str = str(val).strip()
|
|
# Try as a direct ID first
|
|
try:
|
|
acct = env['account.account'].browse(int(val_str))
|
|
if acct.exists():
|
|
return acct.id
|
|
except (ValueError, TypeError):
|
|
pass
|
|
# Try as an account code
|
|
acct = env['account.account'].search([
|
|
('code', '=', val_str),
|
|
('company_ids', 'in', env.company.id),
|
|
], limit=1)
|
|
if acct:
|
|
return acct.id
|
|
return False
|
|
|
|
|
|
def create_payroll_journal_entry(env, params):
|
|
journal_id = int(params['journal_id'])
|
|
date = params['date']
|
|
ref = params.get('ref', 'Payroll Entry')
|
|
lines_data = params['lines']
|
|
|
|
# Duplicate check: same journal + date + ref + similar amount
|
|
total_debit = sum(float(l.get('debit', 0)) for l in lines_data)
|
|
existing = env['account.move'].search([
|
|
('journal_id', '=', journal_id),
|
|
('date', '=', date),
|
|
('ref', 'ilike', ref[:30]),
|
|
('state', 'in', ('draft', 'posted')),
|
|
], limit=1)
|
|
if existing:
|
|
return {
|
|
'status': 'duplicate',
|
|
'error': f'Entry already exists: {existing.name} (ref: {existing.ref}) on {existing.date} '
|
|
f'for ${existing.amount_total:,.2f}. Skipping to avoid duplicate.',
|
|
'existing_move_id': existing.id,
|
|
'existing_name': existing.name,
|
|
}
|
|
|
|
# Resolve account codes to IDs
|
|
resolved_lines = []
|
|
for line in lines_data:
|
|
account_id = _resolve_account_id(env, line['account_id'])
|
|
if not account_id:
|
|
return {'error': f"Account not found: {line['account_id']}. "
|
|
f"Provide a valid account code (e.g. '2201') or database ID."}
|
|
resolved_lines.append((0, 0, {
|
|
'account_id': account_id,
|
|
'name': line.get('name', 'Payroll'),
|
|
'debit': float(line.get('debit', 0)),
|
|
'credit': float(line.get('credit', 0)),
|
|
'partner_id': int(line['partner_id']) if line.get('partner_id') else False,
|
|
}))
|
|
|
|
move_vals = {
|
|
'journal_id': journal_id,
|
|
'date': date,
|
|
'ref': ref,
|
|
'line_ids': resolved_lines,
|
|
}
|
|
move = env['account.move'].create(move_vals)
|
|
return {'status': 'created', 'move_id': move.id, 'name': move.name}
|
|
|
|
|
|
def get_payroll_schedule(env, params):
|
|
return {'status': 'info', 'message': 'Payroll schedule available via fusion_payroll module.'}
|
|
|
|
|
|
def match_payroll_cheques(env, params):
|
|
st_line_id = int(params['statement_line_id'])
|
|
move_line_ids = [int(x) for x in params['move_line_ids']]
|
|
st_line = env['account.bank.statement.line'].browse(st_line_id)
|
|
st_line.set_line_bank_statement_line(move_line_ids)
|
|
return {'status': 'matched', 'statement_line_id': st_line_id}
|
|
|
|
|
|
def verify_payroll_deductions(env, params):
|
|
return verify_source_deductions(env, params)
|
|
|
|
|
|
def get_cra_remittance_due(env, params):
|
|
return get_cra_remittance_status(env, params)
|
|
|
|
|
|
def prepare_cra_payment(env, params):
|
|
return create_payroll_journal_entry(env, params)
|
|
|
|
|
|
def generate_t4(env, params):
|
|
return {'status': 'info', 'message': 'T4 generation available via fusion_payroll module.'}
|
|
|
|
|
|
def generate_roe(env, params):
|
|
return {'status': 'info', 'message': 'ROE generation available via fusion_payroll module.'}
|
|
|
|
|
|
def get_payroll_cost_report(env, params):
|
|
return get_payroll_entries(env, params)
|
|
|
|
|
|
TOOLS = {
|
|
'get_payroll_entries': get_payroll_entries,
|
|
'compare_payroll_to_bank': compare_payroll_to_bank,
|
|
'verify_source_deductions': verify_source_deductions,
|
|
'get_cra_remittance_status': get_cra_remittance_status,
|
|
'find_unmatched_payroll_cheques': find_unmatched_payroll_cheques,
|
|
'parse_payroll_summary': parse_payroll_summary,
|
|
'create_payroll_journal_entry': create_payroll_journal_entry,
|
|
'get_payroll_schedule': get_payroll_schedule,
|
|
'match_payroll_cheques': match_payroll_cheques,
|
|
'verify_payroll_deductions': verify_payroll_deductions,
|
|
'get_cra_remittance_due': get_cra_remittance_due,
|
|
'prepare_cra_payment': prepare_cra_payment,
|
|
'generate_t4': generate_t4,
|
|
'generate_roe': generate_roe,
|
|
'get_payroll_cost_report': get_payroll_cost_report,
|
|
}
|