changes
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
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_ids', 'in', env.company.id),
|
||||
])
|
||||
itc_accounts = env['account.account'].search([
|
||||
('code', '=like', '2006%'), ('company_ids', 'in', 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):
|
||||
"""Route through ReportsAdapter for tri-mode consistency. The Community
|
||||
fallback returns an error dict explaining the report is Enterprise-only."""
|
||||
from ..data_adapters import get_adapter
|
||||
adapter = get_adapter(env, 'reports')
|
||||
return adapter.run_report(
|
||||
ref_id=params.get('report_ref', 'account.generic_tax_report'),
|
||||
date_from=params.get('date_from'),
|
||||
date_to=params.get('date_to'),
|
||||
limit=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_ids', 'in', 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,
|
||||
}
|
||||
Reference in New Issue
Block a user