300 lines
11 KiB
Python
300 lines
11 KiB
Python
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,
|
|
}
|