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, }