import logging _logger = logging.getLogger(__name__) def calculate_hst_balance(env, params): date_from = params.get('date_from') date_to = params.get('date_to') base_domain = [ ('parent_state', '=', 'posted'), ('company_id', '=', env.company.id), ] if date_from: base_domain.append(('date', '>=', date_from)) if date_to: base_domain.append(('date', '<=', date_to)) # Odoo 19 Enterprise: account.account may not have company_id field # (shared chart of accounts). Use try/except to handle both cases. try: collected_accounts = env['account.account'].search([ ('code', '=like', '2005%'), ('company_id', '=', env.company.id), ]) itc_accounts = env['account.account'].search([ ('code', '=like', '2006%'), ('company_id', '=', env.company.id), ]) except Exception: collected_accounts = env['account.account'].search([ ('code', '=like', '2005%'), ]) itc_accounts = env['account.account'].search([ ('code', '=like', '2006%'), ]) collected_lines = env['account.move.line'].search( base_domain + [('account_id', 'in', collected_accounts.ids)] ) itc_lines = env['account.move.line'].search( base_domain + [('account_id', 'in', itc_accounts.ids)] ) hst_collected = abs(sum(l.balance for l in collected_lines)) itcs = abs(sum(l.balance for l in itc_lines)) return { 'hst_collected': hst_collected, 'input_tax_credits': itcs, 'net_hst': hst_collected - itcs, 'status': 'owing' if (hst_collected - itcs) > 0 else 'refund', 'period': f'{date_from or "all"} to {date_to or "now"}', } def get_tax_report(env, params): report_ref = params.get('report_ref', 'account.generic_tax_report') try: report = env.ref(report_ref) except Exception: return {'error': f'Report not found: {report_ref}'} options = report.get_options({ 'date': { 'date_from': params.get('date_from', ''), 'date_to': params.get('date_to', ''), } }) lines = report._get_lines(options) return { 'report_name': report.name, 'lines': [{ 'name': l.get('name', ''), 'columns': [c.get('no_format', c.get('name', '')) for c in l.get('columns', [])], } for l in lines[:50]], } def find_missing_tax_invoices(env, params): date_from = params.get('date_from') date_to = params.get('date_to') domain = [ ('move_type', 'in', ('out_invoice', 'out_refund')), ('state', '=', 'posted'), ('company_id', '=', env.company.id), ] if date_from: domain.append(('date', '>=', date_from)) if date_to: domain.append(('date', '<=', date_to)) invoices = env['account.move'].search(domain) missing = invoices.filtered( lambda inv: not any(line.tax_ids for line in inv.invoice_line_ids) ) return { 'total_invoices': len(invoices), 'missing_tax_count': len(missing), 'invoices': [{ 'id': inv.id, 'name': inv.name, 'partner': inv.partner_id.name if inv.partner_id else '', 'amount_total': inv.amount_total, 'date': str(inv.date), } for inv in missing[:30]], } def find_missing_itc_bills(env, params): date_from = params.get('date_from') date_to = params.get('date_to') domain = [ ('move_type', 'in', ('in_invoice', 'in_refund')), ('state', '=', 'posted'), ('company_id', '=', env.company.id), ] if date_from: domain.append(('date', '>=', date_from)) if date_to: domain.append(('date', '<=', date_to)) bills = env['account.move'].search(domain) missing = bills.filtered( lambda b: not any(line.tax_ids for line in b.invoice_line_ids) ) return { 'total_bills': len(bills), 'missing_itc_count': len(missing), 'bills': [{ 'id': b.id, 'name': b.name, 'partner': b.partner_id.name if b.partner_id else '', 'amount_total': b.amount_total, 'date': str(b.date), } for b in missing[:30]], } def get_tax_return_status(env, params): try: AccountReturn = env['account.return'] except KeyError: return {'error': 'Tax return model (account.return) is not available. The account_tax_report or related Enterprise module may not be installed.'} returns = AccountReturn.search([ ('company_id', '=', env.company.id), ], order='date_start desc', limit=10) return { 'returns': [{ 'id': r.id, 'name': r.display_name, 'date_start': str(r.date_start) if hasattr(r, 'date_start') else '', 'date_end': str(r.date_end) if hasattr(r, 'date_end') else '', 'state': r.state if hasattr(r, 'state') else '', } for r in returns], } def generate_tax_return(env, params): try: AccountReturn = env['account.return'] except KeyError: return {'error': 'Tax return model (account.return) is not available.'} try: AccountReturn._generate_or_refresh_all_returns( company=env.company ) return {'status': 'generated', 'message': 'Tax returns refreshed successfully.'} except Exception as e: return {'error': str(e)} def validate_tax_return(env, params): try: AccountReturn = env['account.return'] except KeyError: return {'error': 'Tax return model (account.return) is not available.'} return_id = int(params['return_id']) tax_return = AccountReturn.browse(return_id) if not tax_return.exists(): return {'error': 'Tax return not found'} try: tax_return.action_validate() return {'status': 'validated', 'return_id': return_id} except Exception as e: return {'error': str(e)} def create_expense_entry(env, params): """[Tier 3] Create a direct GL expense entry in the Misc journal with optional HST split. This is the 'old school' way of recording expenses without a formal vendor bill. Requires user approval before execution.""" date = params.get('date', str(env['account.move']._fields['date'].default(env['account.move']))) description = params.get('description', 'Expense') expense_account_id = int(params['expense_account_id']) amount = abs(float(params['amount'])) has_hst = params.get('has_hst', False) bank_journal_id = int(params.get('bank_journal_id', 0)) # Find the MISC journal misc_journal = env['account.journal'].search([ ('code', '=', 'MISC'), ('company_id', '=', env.company.id), ], limit=1) if not misc_journal: return {'error': 'Miscellaneous Operations journal (MISC) not found'} expense_account = env['account.account'].browse(expense_account_id) if not expense_account.exists(): return {'error': f'Expense account not found: {expense_account_id}'} # Determine credit account (bank outstanding or AP) credit_account = None if bank_journal_id: bank_journal = env['account.journal'].browse(bank_journal_id) if bank_journal.exists(): # Use the bank journal's default debit/credit account credit_account = (bank_journal.default_account_id or bank_journal.company_id.account_journal_payment_credit_account_id) if not credit_account: # Fallback to AP account credit_account = env['account.account'].search([ ('account_type', '=', 'liability_payable'), ('company_id', '=', env.company.id), ], limit=1) if not credit_account: return {'error': 'Could not determine credit account for the expense entry'} line_ids = [] if has_hst: # Split: net expense + 13% HST ITC hst_rate = 0.13 net_amount = round(amount / (1 + hst_rate), 2) hst_amount = round(amount - net_amount, 2) # Find HST ITC account (2006%) itc_account = env['account.account'].search([ ('code', '=like', '2006%'), ], limit=1) if not itc_account: # Fallback: use the HST purchase tax account hst_tax = env['account.tax'].search([ ('type_tax_use', '=', 'purchase'), ('amount', '=', 13.0), ('company_id', '=', env.company.id), ], limit=1) if hst_tax and hst_tax.invoice_repartition_line_ids: for rep in hst_tax.invoice_repartition_line_ids: if rep.repartition_type == 'tax' and rep.account_id: itc_account = rep.account_id break if not itc_account: return {'error': 'HST ITC account (2006) not found'} line_ids = [ (0, 0, {'name': description, 'account_id': expense_account_id, 'debit': net_amount, 'credit': 0.0}), (0, 0, {'name': f'HST ITC - {description}', 'account_id': itc_account.id, 'debit': hst_amount, 'credit': 0.0}), (0, 0, {'name': description, 'account_id': credit_account.id, 'debit': 0.0, 'credit': amount}), ] else: # Simple: debit expense / credit bank line_ids = [ (0, 0, {'name': description, 'account_id': expense_account_id, 'debit': amount, 'credit': 0.0}), (0, 0, {'name': description, 'account_id': credit_account.id, 'debit': 0.0, 'credit': amount}), ] try: move = env['account.move'].create({ 'move_type': 'entry', 'journal_id': misc_journal.id, 'date': date, 'ref': description, 'line_ids': line_ids, 'company_id': env.company.id, }) move.action_post() return { 'status': 'posted', 'move_id': move.id, 'move_name': move.name, 'amount': amount, 'has_hst': has_hst, 'hst_amount': round(amount - amount / 1.13, 2) if has_hst else 0.0, } except Exception as e: _logger.error("Failed to create expense entry: %s", e) return {'error': str(e)} TOOLS = { 'calculate_hst_balance': calculate_hst_balance, 'get_tax_report': get_tax_report, 'find_missing_tax_invoices': find_missing_tax_invoices, 'find_missing_itc_bills': find_missing_itc_bills, 'get_tax_return_status': get_tax_return_status, 'generate_tax_return': generate_tax_return, 'validate_tax_return': validate_tax_return, 'create_expense_entry': create_expense_entry, }