changes
This commit is contained in:
19
fusion_accounting/services/tools/__init__.py
Normal file
19
fusion_accounting/services/tools/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from .bank_reconciliation import TOOLS as BANK_RECON_TOOLS
|
||||
from .hst_management import TOOLS as HST_TOOLS
|
||||
from .accounts_receivable import TOOLS as AR_TOOLS
|
||||
from .accounts_payable import TOOLS as AP_TOOLS
|
||||
from .journal_review import TOOLS as JOURNAL_TOOLS
|
||||
from .month_end import TOOLS as MONTH_END_TOOLS
|
||||
from .payroll import TOOLS as PAYROLL_TOOLS
|
||||
from .inventory import TOOLS as INVENTORY_TOOLS
|
||||
from .adp import TOOLS as ADP_TOOLS
|
||||
from .reporting import TOOLS as REPORTING_TOOLS
|
||||
from .audit import TOOLS as AUDIT_TOOLS
|
||||
|
||||
TOOL_DISPATCH = {}
|
||||
for tools_dict in [
|
||||
BANK_RECON_TOOLS, HST_TOOLS, AR_TOOLS, AP_TOOLS, JOURNAL_TOOLS,
|
||||
MONTH_END_TOOLS, PAYROLL_TOOLS, INVENTORY_TOOLS, ADP_TOOLS,
|
||||
REPORTING_TOOLS, AUDIT_TOOLS,
|
||||
]:
|
||||
TOOL_DISPATCH.update(tools_dict)
|
||||
150
fusion_accounting/services/tools/accounts_payable.py
Normal file
150
fusion_accounting/services/tools/accounts_payable.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
from datetime import timedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_ap_aging(env, params):
|
||||
today = fields.Date.today()
|
||||
domain = [
|
||||
('account_id.account_type', '=', 'liability_payable'),
|
||||
('parent_state', '=', 'posted'),
|
||||
('reconciled', '=', False),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
amls = env['account.move.line'].search(domain)
|
||||
|
||||
buckets = {'current': 0, '1_30': 0, '31_60': 0, '61_90': 0, '90_plus': 0}
|
||||
for aml in amls:
|
||||
amt = abs(aml.amount_residual)
|
||||
if not aml.date_maturity or aml.date_maturity >= today:
|
||||
buckets['current'] += amt
|
||||
else:
|
||||
days = (today - aml.date_maturity).days
|
||||
if days <= 30:
|
||||
buckets['1_30'] += amt
|
||||
elif days <= 60:
|
||||
buckets['31_60'] += amt
|
||||
elif days <= 90:
|
||||
buckets['61_90'] += amt
|
||||
else:
|
||||
buckets['90_plus'] += amt
|
||||
|
||||
return {'total': sum(buckets.values()), 'buckets': buckets, 'line_count': len(amls)}
|
||||
|
||||
|
||||
def find_duplicate_bills(env, params):
|
||||
window_days = int(params.get('window_days', 7))
|
||||
bills = env['account.move'].search([
|
||||
('move_type', 'in', ('in_invoice', 'in_refund')),
|
||||
('state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
], order='partner_id, amount_total, date')
|
||||
|
||||
duplicates = []
|
||||
prev = None
|
||||
for bill in bills:
|
||||
if prev and (
|
||||
prev.partner_id == bill.partner_id
|
||||
and abs(prev.amount_total - bill.amount_total) < 0.01
|
||||
and abs((prev.date - bill.date).days) <= window_days
|
||||
):
|
||||
duplicates.append({
|
||||
'bill_1': {'id': prev.id, 'name': prev.name, 'date': str(prev.date)},
|
||||
'bill_2': {'id': bill.id, 'name': bill.name, 'date': str(bill.date)},
|
||||
'partner': bill.partner_id.name,
|
||||
'amount': bill.amount_total,
|
||||
})
|
||||
prev = bill
|
||||
|
||||
return {'count': len(duplicates), 'duplicates': duplicates[:20]}
|
||||
|
||||
|
||||
def match_bill_to_po(env, params):
|
||||
bill_id = int(params['bill_id'])
|
||||
bill = env['account.move'].browse(bill_id)
|
||||
if not bill.exists():
|
||||
return {'error': 'Bill not found'}
|
||||
matches = []
|
||||
for line in bill.invoice_line_ids:
|
||||
if line.purchase_line_id:
|
||||
matches.append({
|
||||
'bill_line': line.name or '',
|
||||
'po': line.purchase_line_id.order_id.name,
|
||||
'po_line': line.purchase_line_id.name,
|
||||
'po_qty': line.purchase_line_id.product_qty,
|
||||
'bill_qty': line.quantity,
|
||||
'match': abs(line.quantity - line.purchase_line_id.product_qty) < 0.01,
|
||||
})
|
||||
return {'bill': bill.name, 'matches': matches, 'unmatched_lines': len(bill.invoice_line_ids) - len(matches)}
|
||||
|
||||
|
||||
def get_unpaid_bills(env, params):
|
||||
domain = [
|
||||
('move_type', 'in', ('in_invoice', 'in_refund')),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
if params.get('partner_id'):
|
||||
domain.append(('partner_id', '=', int(params['partner_id'])))
|
||||
bills = env['account.move'].search(domain, order='invoice_date_due asc', limit=int(params.get('limit', 50)))
|
||||
return {
|
||||
'count': len(bills),
|
||||
'total': sum(b.amount_residual for b in bills),
|
||||
'bills': [{
|
||||
'id': b.id, 'name': b.name,
|
||||
'partner': b.partner_id.name if b.partner_id else '',
|
||||
'amount_total': b.amount_total,
|
||||
'amount_residual': b.amount_residual,
|
||||
'date_due': str(b.invoice_date_due) if b.invoice_date_due else '',
|
||||
} for b in bills],
|
||||
}
|
||||
|
||||
|
||||
def verify_bill_taxes(env, params):
|
||||
bill_id = int(params['bill_id'])
|
||||
bill = env['account.move'].browse(bill_id)
|
||||
if not bill.exists():
|
||||
return {'error': 'Bill not found'}
|
||||
issues = []
|
||||
for line in bill.invoice_line_ids:
|
||||
if line.product_id and not line.tax_ids:
|
||||
issues.append({
|
||||
'line': line.name or line.product_id.name,
|
||||
'issue': 'No tax applied to product line',
|
||||
})
|
||||
return {'bill': bill.name, 'issues': issues, 'clean': len(issues) == 0}
|
||||
|
||||
|
||||
def get_payment_schedule(env, params):
|
||||
days_ahead = int(params.get('days_ahead', 30))
|
||||
cutoff = fields.Date.today() + timedelta(days=days_ahead)
|
||||
bills = env['account.move'].search([
|
||||
('move_type', '=', 'in_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
('invoice_date_due', '<=', cutoff),
|
||||
('company_id', '=', env.company.id),
|
||||
], order='invoice_date_due asc')
|
||||
return {
|
||||
'period': f'Next {days_ahead} days',
|
||||
'total': sum(b.amount_residual for b in bills),
|
||||
'bills': [{
|
||||
'id': b.id, 'name': b.name,
|
||||
'partner': b.partner_id.name if b.partner_id else '',
|
||||
'amount_residual': b.amount_residual,
|
||||
'date_due': str(b.invoice_date_due) if b.invoice_date_due else '',
|
||||
} for b in bills[:50]],
|
||||
}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_ap_aging': get_ap_aging,
|
||||
'find_duplicate_bills': find_duplicate_bills,
|
||||
'match_bill_to_po': match_bill_to_po,
|
||||
'get_unpaid_bills': get_unpaid_bills,
|
||||
'verify_bill_taxes': verify_bill_taxes,
|
||||
'get_payment_schedule': get_payment_schedule,
|
||||
}
|
||||
165
fusion_accounting/services/tools/accounts_receivable.py
Normal file
165
fusion_accounting/services/tools/accounts_receivable.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_ar_aging(env, params):
|
||||
today = fields.Date.today()
|
||||
domain = [
|
||||
('account_id.account_type', '=', 'asset_receivable'),
|
||||
('parent_state', '=', 'posted'),
|
||||
('reconciled', '=', False),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
amls = env['account.move.line'].search(domain)
|
||||
|
||||
buckets = {'current': 0, '1_30': 0, '31_60': 0, '61_90': 0, '90_plus': 0}
|
||||
for aml in amls:
|
||||
if not aml.date_maturity or aml.date_maturity >= today:
|
||||
buckets['current'] += aml.amount_residual
|
||||
else:
|
||||
days = (today - aml.date_maturity).days
|
||||
if days <= 30:
|
||||
buckets['1_30'] += aml.amount_residual
|
||||
elif days <= 60:
|
||||
buckets['31_60'] += aml.amount_residual
|
||||
elif days <= 90:
|
||||
buckets['61_90'] += aml.amount_residual
|
||||
else:
|
||||
buckets['90_plus'] += aml.amount_residual
|
||||
|
||||
return {
|
||||
'total': sum(buckets.values()),
|
||||
'buckets': buckets,
|
||||
'line_count': len(amls),
|
||||
}
|
||||
|
||||
|
||||
def get_overdue_invoices(env, params):
|
||||
today = fields.Date.today()
|
||||
days_overdue = int(params.get('min_days_overdue', 1))
|
||||
from datetime import timedelta
|
||||
cutoff = today - timedelta(days=days_overdue)
|
||||
invoices = env['account.move'].search([
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
('invoice_date_due', '<', cutoff),
|
||||
('company_id', '=', env.company.id),
|
||||
], order='invoice_date_due asc', limit=int(params.get('limit', 50)))
|
||||
return {
|
||||
'count': len(invoices),
|
||||
'invoices': [{
|
||||
'id': inv.id,
|
||||
'name': inv.name,
|
||||
'partner': inv.partner_id.name if inv.partner_id else '',
|
||||
'email': inv.partner_id.email or '' if inv.partner_id else '',
|
||||
'phone': inv.partner_id.phone or '' if inv.partner_id else '',
|
||||
'amount_total': inv.amount_total,
|
||||
'amount_residual': inv.amount_residual,
|
||||
'date_due': str(inv.invoice_date_due),
|
||||
'days_overdue': (today - inv.invoice_date_due).days,
|
||||
} for inv in invoices],
|
||||
}
|
||||
|
||||
|
||||
def get_partner_balance(env, params):
|
||||
partner_id = int(params['partner_id'])
|
||||
partner = env['res.partner'].browse(partner_id)
|
||||
if not partner.exists():
|
||||
return {'error': 'Partner not found'}
|
||||
amls = env['account.move.line'].search([
|
||||
('partner_id', '=', partner_id),
|
||||
('account_id.account_type', '=', 'asset_receivable'),
|
||||
('parent_state', '=', 'posted'),
|
||||
('reconciled', '=', False),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
return {
|
||||
'partner': partner.name,
|
||||
'balance': sum(aml.amount_residual for aml in amls),
|
||||
'open_items': [{
|
||||
'id': aml.id,
|
||||
'ref': aml.ref or aml.move_id.name,
|
||||
'date': str(aml.date),
|
||||
'amount_residual': aml.amount_residual,
|
||||
'date_maturity': str(aml.date_maturity) if aml.date_maturity else '',
|
||||
} for aml in amls],
|
||||
}
|
||||
|
||||
|
||||
def send_followup(env, params):
|
||||
partner_id = int(params['partner_id'])
|
||||
partner = env['res.partner'].browse(partner_id)
|
||||
if not partner.exists():
|
||||
return {'error': 'Partner not found'}
|
||||
options = {
|
||||
'partner_id': partner_id,
|
||||
'email': params.get('send_email', False),
|
||||
'print': params.get('print_letter', False),
|
||||
'sms': False,
|
||||
}
|
||||
if params.get('email_subject'):
|
||||
options['email_subject'] = params['email_subject']
|
||||
if params.get('body'):
|
||||
options['body'] = params['body']
|
||||
result = partner.execute_followup(options)
|
||||
return {'status': 'sent', 'partner': partner.name, 'result': str(result) if result else 'done'}
|
||||
|
||||
|
||||
def get_followup_report(env, params):
|
||||
partner_id = int(params['partner_id'])
|
||||
partner = env['res.partner'].browse(partner_id)
|
||||
if not partner.exists():
|
||||
return {'error': 'Partner not found'}
|
||||
try:
|
||||
report = env['account.followup.report']
|
||||
html = report._get_followup_report_html(partner)
|
||||
return {'partner': partner.name, 'html': html}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def reconcile_payment_to_invoice(env, params):
|
||||
move_line_ids = [int(x) for x in params['move_line_ids']]
|
||||
amls = env['account.move.line'].browse(move_line_ids)
|
||||
if len(amls) < 2:
|
||||
return {'error': 'Need at least 2 journal items to reconcile'}
|
||||
amls.reconcile()
|
||||
return {
|
||||
'status': 'reconciled',
|
||||
'move_line_ids': move_line_ids,
|
||||
}
|
||||
|
||||
|
||||
def get_unmatched_payments(env, params):
|
||||
domain = [
|
||||
('account_id.account_type', '=', 'asset_receivable'),
|
||||
('parent_state', '=', 'posted'),
|
||||
('reconciled', '=', False),
|
||||
('move_id.payment_id', '!=', False),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
amls = env['account.move.line'].search(domain, order='date desc')
|
||||
return {
|
||||
'count': len(amls),
|
||||
'payments': [{
|
||||
'id': aml.id,
|
||||
'date': str(aml.date),
|
||||
'ref': aml.ref or aml.move_id.name,
|
||||
'partner': aml.partner_id.name if aml.partner_id else '',
|
||||
'amount': abs(aml.amount_residual),
|
||||
} for aml in amls[:50]],
|
||||
}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_ar_aging': get_ar_aging,
|
||||
'get_overdue_invoices': get_overdue_invoices,
|
||||
'get_partner_balance': get_partner_balance,
|
||||
'send_followup': send_followup,
|
||||
'get_followup_report': get_followup_report,
|
||||
'reconcile_payment_to_invoice': reconcile_payment_to_invoice,
|
||||
'get_unmatched_payments': get_unmatched_payments,
|
||||
}
|
||||
111
fusion_accounting/services/tools/adp.py
Normal file
111
fusion_accounting/services/tools/adp.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_adp_receivable_aging(env, params):
|
||||
accounts = env['account.account'].search([
|
||||
('code', '=like', '1101%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
today = fields.Date.today()
|
||||
amls = env['account.move.line'].search([
|
||||
('account_id', 'in', accounts.ids),
|
||||
('reconciled', '=', False),
|
||||
('parent_state', '=', 'posted'),
|
||||
])
|
||||
buckets = {'current': 0, '1_30': 0, '31_60': 0, '61_90': 0, '90_plus': 0}
|
||||
for aml in amls:
|
||||
amt = abs(aml.amount_residual)
|
||||
if not aml.date_maturity or aml.date_maturity >= today:
|
||||
buckets['current'] += amt
|
||||
else:
|
||||
days = (today - aml.date_maturity).days
|
||||
if days <= 30:
|
||||
buckets['1_30'] += amt
|
||||
elif days <= 60:
|
||||
buckets['31_60'] += amt
|
||||
elif days <= 90:
|
||||
buckets['61_90'] += amt
|
||||
else:
|
||||
buckets['90_plus'] += amt
|
||||
return {'total': sum(buckets.values()), 'buckets': buckets}
|
||||
|
||||
|
||||
def match_adp_payment_to_invoice(env, params):
|
||||
move_line_ids = [int(x) for x in params['move_line_ids']]
|
||||
amls = env['account.move.line'].browse(move_line_ids).exists()
|
||||
if len(amls) < 2:
|
||||
return {'error': 'Need at least 2 existing journal items to reconcile'}
|
||||
amls.reconcile()
|
||||
return {'status': 'matched', 'move_line_ids': amls.ids}
|
||||
|
||||
|
||||
def verify_adp_split(env, params):
|
||||
invoice_id = int(params['invoice_id'])
|
||||
invoice = env['account.move'].browse(invoice_id)
|
||||
if not invoice.exists():
|
||||
return {'error': 'Invoice not found'}
|
||||
lines = invoice.invoice_line_ids
|
||||
total = invoice.amount_untaxed
|
||||
return {
|
||||
'invoice': invoice.name,
|
||||
'total_untaxed': total,
|
||||
'total_with_tax': invoice.amount_total,
|
||||
'lines': [{'name': l.name, 'subtotal': l.price_subtotal, 'total': l.price_total} for l in lines],
|
||||
'balanced': abs(sum(l.price_subtotal for l in lines) - total) < 0.01,
|
||||
}
|
||||
|
||||
|
||||
def find_adp_without_payment(env, params):
|
||||
adp_partner = env['res.partner'].search([('name', 'ilike', 'ADP')], limit=1)
|
||||
if not adp_partner:
|
||||
return {'status': 'info', 'message': 'No ADP partner found in the system.'}
|
||||
invoices = env['account.move'].search([
|
||||
('partner_id', '=', adp_partner.id),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
])
|
||||
return {
|
||||
'count': len(invoices),
|
||||
'invoices': [{
|
||||
'id': inv.id, 'name': inv.name,
|
||||
'amount': inv.amount_residual, 'date': str(inv.date),
|
||||
} for inv in invoices[:20]],
|
||||
}
|
||||
|
||||
|
||||
def get_adp_summary(env, params):
|
||||
date_from = params.get('date_from')
|
||||
date_to = params.get('date_to')
|
||||
accounts = env['account.account'].search([
|
||||
('code', '=like', '1101%'), ('company_id', '=', env.company.id),
|
||||
])
|
||||
domain = [
|
||||
('account_id', 'in', accounts.ids),
|
||||
('parent_state', '=', 'posted'),
|
||||
]
|
||||
if date_from:
|
||||
domain.append(('date', '>=', date_from))
|
||||
if date_to:
|
||||
domain.append(('date', '<=', date_to))
|
||||
lines = env['account.move.line'].search(domain)
|
||||
total_debit = sum(l.debit for l in lines)
|
||||
total_credit = sum(l.credit for l in lines)
|
||||
return {
|
||||
'period': f'{date_from or "all"} to {date_to or "now"}',
|
||||
'billed': total_debit,
|
||||
'collected': total_credit,
|
||||
'outstanding': total_debit - total_credit,
|
||||
}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_adp_receivable_aging': get_adp_receivable_aging,
|
||||
'match_adp_payment_to_invoice': match_adp_payment_to_invoice,
|
||||
'verify_adp_split': verify_adp_split,
|
||||
'find_adp_without_payment': find_adp_without_payment,
|
||||
'get_adp_summary': get_adp_summary,
|
||||
}
|
||||
156
fusion_accounting/services/tools/audit.py
Normal file
156
fusion_accounting/services/tools/audit.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def audit_posted_entry(env, params):
|
||||
move_id = int(params['move_id'])
|
||||
move = env['account.move'].browse(move_id)
|
||||
if not move.exists():
|
||||
return {'error': 'Entry not found'}
|
||||
issues = []
|
||||
total_debit = sum(l.debit for l in move.line_ids)
|
||||
total_credit = sum(l.credit for l in move.line_ids)
|
||||
if abs(total_debit - total_credit) > 0.01:
|
||||
issues.append({'severity': 'critical', 'issue': f'Unbalanced entry: debit={total_debit}, credit={total_credit}'})
|
||||
for line in move.line_ids:
|
||||
if not line.account_id:
|
||||
issues.append({'severity': 'critical', 'issue': f'Line missing account: {line.name}'})
|
||||
if not move.line_ids:
|
||||
issues.append({'severity': 'warning', 'issue': 'Entry has no lines'})
|
||||
return {
|
||||
'move': move.name, 'date': str(move.date),
|
||||
'issues': issues, 'clean': len(issues) == 0,
|
||||
}
|
||||
|
||||
|
||||
def audit_account_balances(env, params):
|
||||
from .journal_review import find_wrong_direction_balances
|
||||
return find_wrong_direction_balances(env, params)
|
||||
|
||||
|
||||
def audit_tax_compliance(env, params):
|
||||
from .hst_management import find_missing_tax_invoices, find_missing_itc_bills
|
||||
invoices = find_missing_tax_invoices(env, params)
|
||||
bills = find_missing_itc_bills(env, params)
|
||||
return {
|
||||
'missing_tax_invoices': invoices.get('missing_tax_count', 0),
|
||||
'missing_itc_bills': bills.get('missing_itc_count', 0),
|
||||
'total_issues': invoices.get('missing_tax_count', 0) + bills.get('missing_itc_count', 0),
|
||||
}
|
||||
|
||||
|
||||
def audit_reconciliation_integrity(env, params):
|
||||
from .journal_review import verify_reconciliation_integrity
|
||||
return verify_reconciliation_integrity(env, params)
|
||||
|
||||
|
||||
def check_hash_chain(env, params):
|
||||
from .month_end import run_hash_integrity_check
|
||||
return run_hash_integrity_check(env, params)
|
||||
|
||||
|
||||
def check_sequence_gaps(env, params):
|
||||
from .journal_review import find_sequence_gaps
|
||||
return find_sequence_gaps(env, params)
|
||||
|
||||
|
||||
def flag_entry(env, params):
|
||||
move_id = int(params['move_id'])
|
||||
flag = params.get('flag', 'Review Required')
|
||||
recommendation = params.get('recommendation', '')
|
||||
move = env['account.move'].browse(move_id)
|
||||
if not move.exists():
|
||||
return {'error': 'Entry not found'}
|
||||
body = f'<strong>🏴 {flag}</strong><br/>{recommendation}'
|
||||
move.message_post(body=body, message_type='comment', subtype_xmlid='mail.mt_note')
|
||||
return {'status': 'flagged', 'move': move.name, 'flag': flag}
|
||||
|
||||
|
||||
def get_audit_status(env, params):
|
||||
statuses = env['account.audit.account.status'].search([])
|
||||
return {
|
||||
'statuses': [{
|
||||
'id': s.id,
|
||||
'account': s.account_id.name,
|
||||
'status': s.status,
|
||||
'audit': s.audit_id.display_name if s.audit_id else '',
|
||||
} for s in statuses[:50]],
|
||||
}
|
||||
|
||||
|
||||
def set_audit_status(env, params):
|
||||
status_id = int(params['status_id'])
|
||||
new_status = params['status']
|
||||
rec = env['account.audit.account.status'].browse(status_id)
|
||||
if not rec.exists():
|
||||
return {'error': 'Audit status record not found'}
|
||||
rec.status = new_status
|
||||
return {'status': 'updated', 'id': status_id, 'new_status': new_status}
|
||||
|
||||
|
||||
def get_audit_trail(env, params):
|
||||
move_id = int(params['move_id'])
|
||||
move = env['account.move'].browse(move_id)
|
||||
if not move.exists():
|
||||
return {'error': 'Entry not found'}
|
||||
messages = env['mail.message'].search([
|
||||
('model', '=', 'account.move'),
|
||||
('res_id', '=', move_id),
|
||||
], order='date desc', limit=20)
|
||||
return {
|
||||
'move': move.name,
|
||||
'messages': [{
|
||||
'date': str(m.date),
|
||||
'author': m.author_id.name if m.author_id else '',
|
||||
'body': m.body or '',
|
||||
'type': m.message_type,
|
||||
} for m in messages],
|
||||
}
|
||||
|
||||
|
||||
def run_full_audit(env, params):
|
||||
results = {}
|
||||
results['account_balances'] = audit_account_balances(env, params)
|
||||
results['tax_compliance'] = audit_tax_compliance(env, params)
|
||||
results['reconciliation'] = audit_reconciliation_integrity(env, params)
|
||||
results['hash_chain'] = check_hash_chain(env, params)
|
||||
results['sequence_gaps'] = check_sequence_gaps(env, params)
|
||||
|
||||
total_issues = 0
|
||||
for key, val in results.items():
|
||||
total_issues += val.get('count', 0) + val.get('total_issues', 0)
|
||||
|
||||
score = max(0, 100 - total_issues * 5)
|
||||
return {
|
||||
'score': min(100, score),
|
||||
'total_issues': total_issues,
|
||||
'details': results,
|
||||
}
|
||||
|
||||
|
||||
def get_audit_report(env, params):
|
||||
audit = run_full_audit(env, params)
|
||||
report_lines = [f"Audit Score: {audit['score']}/100", f"Total Issues: {audit['total_issues']}", '']
|
||||
for domain, detail in audit.get('details', {}).items():
|
||||
report_lines.append(f"--- {domain.replace('_', ' ').title()} ---")
|
||||
count = detail.get('count', detail.get('total_issues', 0))
|
||||
report_lines.append(f" Issues: {count}")
|
||||
return {'report': '\n'.join(report_lines), 'score': audit['score']}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'audit_posted_entry': audit_posted_entry,
|
||||
'audit_account_balances': audit_account_balances,
|
||||
'audit_tax_compliance': audit_tax_compliance,
|
||||
'audit_reconciliation_integrity': audit_reconciliation_integrity,
|
||||
'check_hash_chain': check_hash_chain,
|
||||
'check_sequence_gaps': check_sequence_gaps,
|
||||
'flag_entry': flag_entry,
|
||||
'get_audit_status': get_audit_status,
|
||||
'set_audit_status': set_audit_status,
|
||||
'get_audit_trail': get_audit_trail,
|
||||
'run_full_audit': run_full_audit,
|
||||
'get_audit_report': get_audit_report,
|
||||
}
|
||||
177
fusion_accounting/services/tools/bank_reconciliation.py
Normal file
177
fusion_accounting/services/tools/bank_reconciliation.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_unreconciled_bank_lines(env, params):
|
||||
domain = [('is_reconciled', '=', False), ('company_id', '=', env.company.id)]
|
||||
if params.get('journal_id'):
|
||||
domain.append(('journal_id', '=', int(params['journal_id'])))
|
||||
if params.get('date_from'):
|
||||
domain.append(('date', '>=', params['date_from']))
|
||||
if params.get('date_to'):
|
||||
domain.append(('date', '<=', params['date_to']))
|
||||
if params.get('min_amount'):
|
||||
domain.append(('amount', '>=', float(params['min_amount'])))
|
||||
limit = int(params.get('limit', 50))
|
||||
lines = env['account.bank.statement.line'].search(domain, limit=limit, order='date desc')
|
||||
return {
|
||||
'count': len(lines),
|
||||
'total_amount': sum(abs(l.amount) for l in lines),
|
||||
'lines': [{
|
||||
'id': l.id,
|
||||
'date': str(l.date),
|
||||
'payment_ref': l.payment_ref or '',
|
||||
'partner_name': l.partner_name or (l.partner_id.name if l.partner_id else ''),
|
||||
'amount': l.amount,
|
||||
'journal': l.journal_id.name,
|
||||
} for l in lines],
|
||||
}
|
||||
|
||||
|
||||
def get_unreconciled_receipts(env, params):
|
||||
account_code = params.get('account_code', '1122')
|
||||
accounts = env['account.account'].search([
|
||||
('code', '=like', f'{account_code}%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
domain = [
|
||||
('account_id', 'in', accounts.ids),
|
||||
('reconciled', '=', False),
|
||||
('parent_state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
lines = env['account.move.line'].search(domain, order='date desc')
|
||||
return {
|
||||
'count': len(lines),
|
||||
'total_amount': sum(abs(l.amount_residual) for l in lines),
|
||||
'lines': [{
|
||||
'id': l.id,
|
||||
'date': str(l.date),
|
||||
'ref': l.ref or l.move_id.name,
|
||||
'partner': l.partner_id.name if l.partner_id else '',
|
||||
'amount_residual': l.amount_residual,
|
||||
} for l in lines],
|
||||
}
|
||||
|
||||
|
||||
def match_bank_line_to_payments(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)
|
||||
if not st_line.exists():
|
||||
return {'error': 'Statement line not found'}
|
||||
st_line.set_line_bank_statement_line(move_line_ids)
|
||||
return {
|
||||
'status': 'matched',
|
||||
'statement_line_id': st_line_id,
|
||||
'matched_move_lines': move_line_ids,
|
||||
'is_reconciled': st_line.is_reconciled,
|
||||
}
|
||||
|
||||
|
||||
def auto_reconcile_bank_lines(env, params):
|
||||
company_id = params.get('company_id', env.company.id)
|
||||
lines = env['account.bank.statement.line'].search([
|
||||
('is_reconciled', '=', False),
|
||||
('company_id', '=', int(company_id)),
|
||||
])
|
||||
before_count = len(lines)
|
||||
lines._try_auto_reconcile_statement_lines(company_id=int(company_id))
|
||||
still_unreconciled = env['account.bank.statement.line'].search([
|
||||
('is_reconciled', '=', False),
|
||||
('company_id', '=', int(company_id)),
|
||||
])
|
||||
reconciled_count = before_count - len(still_unreconciled)
|
||||
return {
|
||||
'status': 'completed',
|
||||
'lines_before': before_count,
|
||||
'lines_reconciled': reconciled_count,
|
||||
'lines_remaining': len(still_unreconciled),
|
||||
}
|
||||
|
||||
|
||||
def apply_reconcile_model(env, params):
|
||||
model_id = int(params['model_id'])
|
||||
st_line_id = int(params['statement_line_id'])
|
||||
reco_model = env['account.reconcile.model'].browse(model_id)
|
||||
st_line = env['account.bank.statement.line'].browse(st_line_id)
|
||||
if not reco_model.exists() or not st_line.exists():
|
||||
return {'error': 'Model or statement line not found'}
|
||||
_liquidity_lines, suspense_lines, _other_lines = st_line._seek_for_lines()
|
||||
residual = sum(l.amount_residual for l in suspense_lines) if suspense_lines else st_line.amount
|
||||
write_off_vals = reco_model._get_write_off_move_lines_dict(st_line, residual)
|
||||
if write_off_vals:
|
||||
line_ids_create_command = [(0, 0, vals) for vals in write_off_vals]
|
||||
st_line.move_id.write({'line_ids': line_ids_create_command})
|
||||
return {
|
||||
'status': 'applied',
|
||||
'model': reco_model.name,
|
||||
'write_off_lines': len(write_off_vals) if write_off_vals else 0,
|
||||
}
|
||||
|
||||
|
||||
def unmatch_bank_line(env, params):
|
||||
st_line_id = int(params['statement_line_id'])
|
||||
st_line = env['account.bank.statement.line'].browse(st_line_id)
|
||||
if not st_line.exists():
|
||||
return {'error': 'Statement line not found'}
|
||||
st_line.action_unreconcile_entry()
|
||||
return {'status': 'unmatched', 'statement_line_id': st_line_id}
|
||||
|
||||
|
||||
def get_reconcile_suggestions(env, params):
|
||||
st_line_id = int(params['statement_line_id'])
|
||||
st_line = env['account.bank.statement.line'].browse(st_line_id)
|
||||
if not st_line.exists():
|
||||
return {'error': 'Statement line not found'}
|
||||
models = env['account.reconcile.model'].search([
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
return {
|
||||
'models': [{
|
||||
'id': m.id,
|
||||
'name': m.name,
|
||||
'trigger': m.trigger if hasattr(m, 'trigger') else 'manual',
|
||||
} for m in models],
|
||||
}
|
||||
|
||||
|
||||
def sum_payments_by_date(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'}
|
||||
journal_ids = params.get('journal_ids', [])
|
||||
domain = [
|
||||
('parent_state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
('date', '>=', date_from),
|
||||
('date', '<=', date_to),
|
||||
]
|
||||
if journal_ids:
|
||||
domain.append(('journal_id', 'in', [int(j) for j in journal_ids]))
|
||||
lines = env['account.move.line'].search(domain)
|
||||
total_debit = sum(l.debit for l in lines)
|
||||
total_credit = sum(l.credit for l in lines)
|
||||
return {
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'total_debit': total_debit,
|
||||
'total_credit': total_credit,
|
||||
'net': total_debit - total_credit,
|
||||
'line_count': len(lines),
|
||||
}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_unreconciled_bank_lines': get_unreconciled_bank_lines,
|
||||
'get_unreconciled_receipts': get_unreconciled_receipts,
|
||||
'match_bank_line_to_payments': match_bank_line_to_payments,
|
||||
'auto_reconcile_bank_lines': auto_reconcile_bank_lines,
|
||||
'apply_reconcile_model': apply_reconcile_model,
|
||||
'unmatch_bank_line': unmatch_bank_line,
|
||||
'get_reconcile_suggestions': get_reconcile_suggestions,
|
||||
'sum_payments_by_date': sum_payments_by_date,
|
||||
}
|
||||
171
fusion_accounting/services/tools/hst_management.py
Normal file
171
fusion_accounting/services/tools/hst_management.py
Normal file
@@ -0,0 +1,171 @@
|
||||
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))
|
||||
|
||||
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),
|
||||
])
|
||||
|
||||
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):
|
||||
returns = env['account.return'].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:
|
||||
env['account.return']._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):
|
||||
return_id = int(params['return_id'])
|
||||
tax_return = env['account.return'].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)}
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
113
fusion_accounting/services/tools/inventory.py
Normal file
113
fusion_accounting/services/tools/inventory.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_stock_valuation(env, params):
|
||||
accounts = env['account.account'].search([
|
||||
('code', '=like', '1069%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
result = []
|
||||
for acct in 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, 'total': sum(r['balance'] for r in result)}
|
||||
|
||||
|
||||
def get_price_differences(env, params):
|
||||
accounts = env['account.account'].search([
|
||||
('code', '=like', '5010%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
domain = [
|
||||
('account_id', 'in', accounts.ids),
|
||||
('parent_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']))
|
||||
lines = env['account.move.line'].search(domain, order='date desc', limit=50)
|
||||
return {
|
||||
'total': sum(l.balance for l in lines),
|
||||
'entries': [{
|
||||
'id': l.id, 'date': str(l.date),
|
||||
'move': l.move_id.name, 'amount': l.balance,
|
||||
'partner': l.partner_id.name if l.partner_id else '',
|
||||
} for l in lines],
|
||||
}
|
||||
|
||||
|
||||
def get_cogs_ratio_by_category(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))
|
||||
|
||||
revenue_lines = env['account.move.line'].search(
|
||||
base_domain + [('account_id.account_type', '=', 'income')]
|
||||
)
|
||||
cogs_lines = env['account.move.line'].search(
|
||||
base_domain + [('account_id.account_type', '=', 'expense_direct_cost')]
|
||||
)
|
||||
revenue = abs(sum(l.balance for l in revenue_lines))
|
||||
cogs = abs(sum(l.balance for l in cogs_lines))
|
||||
ratio = (cogs / revenue * 100) if revenue else 0
|
||||
return {'revenue': revenue, 'cogs': cogs, 'ratio_pct': round(ratio, 2)}
|
||||
|
||||
|
||||
def find_unusual_adjustments(env, params):
|
||||
threshold = float(params.get('threshold', 1000))
|
||||
domain = [
|
||||
('parent_state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
('account_id.account_type', '=', 'expense_direct_cost'),
|
||||
]
|
||||
lines = env['account.move.line'].search(domain)
|
||||
unusual = lines.filtered(lambda l: abs(l.balance) > threshold)
|
||||
return {
|
||||
'count': len(unusual),
|
||||
'adjustments': [{
|
||||
'id': l.id, 'date': str(l.date), 'move': l.move_id.name,
|
||||
'amount': l.balance, 'name': l.name or '',
|
||||
} for l in unusual[:20]],
|
||||
}
|
||||
|
||||
|
||||
def get_inventory_turnover(env, params):
|
||||
from .reporting import get_profit_loss
|
||||
pl = get_profit_loss(env, params)
|
||||
stock = get_stock_valuation(env, params)
|
||||
avg_inventory = stock.get('total', 0)
|
||||
cogs = 0
|
||||
for line in pl.get('lines', []):
|
||||
if 'cost' in line.get('name', '').lower():
|
||||
cols = line.get('columns', [])
|
||||
if cols:
|
||||
try:
|
||||
cogs = float(cols[0])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
turnover = (cogs / avg_inventory) if avg_inventory else 0
|
||||
return {'cogs': cogs, 'avg_inventory': avg_inventory, 'turnover': round(turnover, 2)}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_stock_valuation': get_stock_valuation,
|
||||
'get_price_differences': get_price_differences,
|
||||
'get_cogs_ratio_by_category': get_cogs_ratio_by_category,
|
||||
'find_unusual_adjustments': find_unusual_adjustments,
|
||||
'get_inventory_turnover': get_inventory_turnover,
|
||||
}
|
||||
220
fusion_accounting/services/tools/journal_review.py
Normal file
220
fusion_accounting/services/tools/journal_review.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
ACCOUNT_TYPE_EXPECTED_DIRECTION = {
|
||||
'asset_receivable': 'debit',
|
||||
'asset_cash': 'debit',
|
||||
'asset_current': 'debit',
|
||||
'asset_non_current': 'debit',
|
||||
'asset_prepayments': 'debit',
|
||||
'asset_fixed': 'debit',
|
||||
'liability_payable': 'credit',
|
||||
'liability_credit_card': 'credit',
|
||||
'liability_current': 'credit',
|
||||
'liability_non_current': 'credit',
|
||||
'equity': 'credit',
|
||||
'equity_unaffected': 'credit',
|
||||
'income': 'credit',
|
||||
'income_other': 'credit',
|
||||
'expense': 'debit',
|
||||
'expense_depreciation': 'debit',
|
||||
'expense_direct_cost': 'debit',
|
||||
'off_balance': None,
|
||||
}
|
||||
|
||||
|
||||
def find_wrong_direction_balances(env, params):
|
||||
balance_data = env['account.move.line'].read_group(
|
||||
[('parent_state', '=', 'posted'), ('company_id', '=', env.company.id)],
|
||||
['balance:sum'], ['account_id'],
|
||||
)
|
||||
acct_ids = [row['account_id'][0] for row in balance_data if row.get('account_id')]
|
||||
acct_map = {}
|
||||
if acct_ids:
|
||||
for acct in env['account.account'].browse(acct_ids):
|
||||
acct_map[acct.id] = acct
|
||||
|
||||
issues = []
|
||||
for row in balance_data:
|
||||
if not row.get('account_id'):
|
||||
continue
|
||||
acct = acct_map.get(row['account_id'][0])
|
||||
if not acct:
|
||||
continue
|
||||
expected = ACCOUNT_TYPE_EXPECTED_DIRECTION.get(acct.account_type)
|
||||
if not expected:
|
||||
continue
|
||||
balance = row.get('balance', 0) or 0
|
||||
if (expected == 'debit' and balance < -0.01) or (expected == 'credit' and balance > 0.01):
|
||||
issues.append({
|
||||
'account_id': acct.id,
|
||||
'code': acct.code,
|
||||
'name': acct.name,
|
||||
'balance': balance,
|
||||
'expected': expected,
|
||||
'actual': 'credit' if balance < 0 else 'debit',
|
||||
})
|
||||
return {'count': len(issues), 'issues': issues}
|
||||
|
||||
|
||||
def find_duplicate_entries(env, params):
|
||||
date_from = params.get('date_from')
|
||||
date_to = params.get('date_to')
|
||||
domain = [
|
||||
('state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
if date_from:
|
||||
domain.append(('date', '>=', date_from))
|
||||
if date_to:
|
||||
domain.append(('date', '<=', date_to))
|
||||
moves = env['account.move'].search(domain, order='partner_id, amount_total, date')
|
||||
|
||||
duplicates = []
|
||||
prev = None
|
||||
for move in moves:
|
||||
if prev and (
|
||||
prev.partner_id == move.partner_id and prev.partner_id
|
||||
and abs(prev.amount_total - move.amount_total) < 0.01
|
||||
and prev.date == move.date
|
||||
and prev.journal_id == move.journal_id
|
||||
):
|
||||
duplicates.append({
|
||||
'entry_1': {'id': prev.id, 'name': prev.name},
|
||||
'entry_2': {'id': move.id, 'name': move.name},
|
||||
'partner': move.partner_id.name,
|
||||
'amount': move.amount_total,
|
||||
'date': str(move.date),
|
||||
})
|
||||
prev = move
|
||||
return {'count': len(duplicates), 'duplicates': duplicates[:20]}
|
||||
|
||||
|
||||
def find_wrong_account_entries(env, params):
|
||||
date_from = params.get('date_from')
|
||||
date_to = params.get('date_to')
|
||||
domain = [
|
||||
('parent_state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
]
|
||||
if date_from:
|
||||
domain.append(('date', '>=', date_from))
|
||||
if date_to:
|
||||
domain.append(('date', '<=', date_to))
|
||||
|
||||
issues = []
|
||||
tax_accounts = env['account.account'].search([
|
||||
('account_type', 'in', ('liability_current', 'asset_current')),
|
||||
('code', '=like', '2005%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
if tax_accounts:
|
||||
revenue_on_tax = env['account.move.line'].search(
|
||||
domain + [
|
||||
('account_id', 'in', tax_accounts.ids),
|
||||
('product_id', '!=', False),
|
||||
]
|
||||
)
|
||||
for line in revenue_on_tax[:20]:
|
||||
issues.append({
|
||||
'id': line.id,
|
||||
'move': line.move_id.name,
|
||||
'account': f'{line.account_id.code} {line.account_id.name}',
|
||||
'product': line.product_id.name,
|
||||
'amount': line.balance,
|
||||
'issue': 'Product line on tax account',
|
||||
})
|
||||
return {'count': len(issues), 'issues': issues}
|
||||
|
||||
|
||||
def find_sequence_gaps(env, params):
|
||||
moves = env['account.move'].search([
|
||||
('state', '=', 'posted'),
|
||||
('company_id', '=', env.company.id),
|
||||
('made_sequence_gap', '=', True),
|
||||
], order='date desc', limit=50)
|
||||
return {
|
||||
'count': len(moves),
|
||||
'gaps': [{
|
||||
'id': m.id,
|
||||
'name': m.name,
|
||||
'date': str(m.date),
|
||||
'journal': m.journal_id.name,
|
||||
} for m in moves],
|
||||
}
|
||||
|
||||
|
||||
def find_draft_entries(env, params):
|
||||
min_age_days = int(params.get('min_age_days', 30))
|
||||
from datetime import timedelta
|
||||
cutoff = fields.Date.today() - timedelta(days=min_age_days)
|
||||
drafts = env['account.move'].search([
|
||||
('state', '=', 'draft'),
|
||||
('date', '<=', cutoff),
|
||||
('company_id', '=', env.company.id),
|
||||
], order='date asc', limit=50)
|
||||
return {
|
||||
'count': len(drafts),
|
||||
'entries': [{
|
||||
'id': d.id,
|
||||
'name': d.name or 'Draft',
|
||||
'date': str(d.date),
|
||||
'journal': d.journal_id.name,
|
||||
'amount': d.amount_total,
|
||||
'partner': d.partner_id.name if d.partner_id else '',
|
||||
} for d in drafts],
|
||||
}
|
||||
|
||||
|
||||
def find_unreconciled_suspense(env, params):
|
||||
suspense_accounts = env['account.account'].search([
|
||||
('code', '=like', '999%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
issues = []
|
||||
for acct in suspense_accounts:
|
||||
balance = sum(env['account.move.line'].search([
|
||||
('account_id', '=', acct.id),
|
||||
('parent_state', '=', 'posted'),
|
||||
]).mapped('balance'))
|
||||
if abs(balance) > 0.01:
|
||||
issues.append({
|
||||
'account_id': acct.id,
|
||||
'code': acct.code,
|
||||
'name': acct.name,
|
||||
'balance': balance,
|
||||
})
|
||||
return {'count': len(issues), 'accounts': issues}
|
||||
|
||||
|
||||
def verify_reconciliation_integrity(env, params):
|
||||
partials = env['account.partial.reconcile'].search([
|
||||
('company_id', '=', env.company.id),
|
||||
], limit=500)
|
||||
issues = []
|
||||
for p in partials:
|
||||
debit_ok = p.debit_move_id.reconciled or abs(p.debit_move_id.amount_residual) < 0.01
|
||||
credit_ok = p.credit_move_id.reconciled or abs(p.credit_move_id.amount_residual) < 0.01
|
||||
if not debit_ok and not credit_ok:
|
||||
issues.append({
|
||||
'id': p.id,
|
||||
'debit_move': p.debit_move_id.move_id.name,
|
||||
'credit_move': p.credit_move_id.move_id.name,
|
||||
'amount': p.amount,
|
||||
'debit_residual': p.debit_move_id.amount_residual,
|
||||
'credit_residual': p.credit_move_id.amount_residual,
|
||||
})
|
||||
return {'count': len(issues), 'issues': issues[:20]}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'find_wrong_direction_balances': find_wrong_direction_balances,
|
||||
'find_duplicate_entries': find_duplicate_entries,
|
||||
'find_wrong_account_entries': find_wrong_account_entries,
|
||||
'find_sequence_gaps': find_sequence_gaps,
|
||||
'find_draft_entries': find_draft_entries,
|
||||
'find_unreconciled_suspense': find_unreconciled_suspense,
|
||||
'verify_reconciliation_integrity': verify_reconciliation_integrity,
|
||||
}
|
||||
130
fusion_accounting/services/tools/month_end.py
Normal file
130
fusion_accounting/services/tools/month_end.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import logging
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_close_checklist(env, params):
|
||||
from .bank_reconciliation import get_unreconciled_bank_lines
|
||||
from .journal_review import find_draft_entries, find_sequence_gaps
|
||||
from .hst_management import calculate_hst_balance
|
||||
|
||||
period = params.get('period', str(fields.Date.today())[:7])
|
||||
date_from = f'{period}-01'
|
||||
import calendar
|
||||
year, month = int(period[:4]), int(period[5:7])
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
date_to = f'{period}-{last_day:02d}'
|
||||
|
||||
p = {'date_from': date_from, 'date_to': date_to}
|
||||
|
||||
bank = get_unreconciled_bank_lines(env, p)
|
||||
drafts = find_draft_entries(env, {'min_age_days': '0'})
|
||||
gaps = find_sequence_gaps(env, p)
|
||||
hst = calculate_hst_balance(env, p)
|
||||
|
||||
checklist = [
|
||||
{'item': 'Bank Reconciliation', 'status': 'ok' if bank['count'] == 0 else 'attention', 'detail': f"{bank['count']} unreconciled lines"},
|
||||
{'item': 'Draft Entries', 'status': 'ok' if drafts['count'] == 0 else 'attention', 'detail': f"{drafts['count']} draft entries"},
|
||||
{'item': 'Sequence Gaps', 'status': 'ok' if gaps['count'] == 0 else 'warning', 'detail': f"{gaps['count']} gaps found"},
|
||||
{'item': 'HST Balance', 'status': 'info', 'detail': f"Net HST: ${hst['net_hst']:.2f}"},
|
||||
]
|
||||
return {'period': period, 'checklist': checklist}
|
||||
|
||||
|
||||
def get_unreconciled_counts(env, params):
|
||||
accounts = env['account.account'].search([
|
||||
('reconcile', '=', True),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
result = []
|
||||
for acct in accounts:
|
||||
count = env['account.move.line'].search_count([
|
||||
('account_id', '=', acct.id),
|
||||
('reconciled', '=', False),
|
||||
('parent_state', '=', 'posted'),
|
||||
])
|
||||
if count > 0:
|
||||
result.append({
|
||||
'account_id': acct.id,
|
||||
'code': acct.code,
|
||||
'name': acct.name,
|
||||
'unreconciled_count': count,
|
||||
})
|
||||
return {'accounts': sorted(result, key=lambda x: -x['unreconciled_count'])}
|
||||
|
||||
|
||||
def find_entries_in_locked_period(env, params):
|
||||
company = env.company
|
||||
lock_date = company.fiscalyear_lock_date
|
||||
if not lock_date:
|
||||
return {'status': 'no_lock_date', 'entries': []}
|
||||
entries = env['account.move'].search([
|
||||
('date', '<=', lock_date),
|
||||
('state', '=', 'draft'),
|
||||
('company_id', '=', company.id),
|
||||
])
|
||||
return {
|
||||
'lock_date': str(lock_date),
|
||||
'count': len(entries),
|
||||
'entries': [{'id': e.id, 'name': e.name, 'date': str(e.date)} for e in entries[:20]],
|
||||
}
|
||||
|
||||
|
||||
def get_accrual_status(env, params):
|
||||
accrual_codes = params.get('account_codes', ['2100', '2110', '2120'])
|
||||
result = []
|
||||
for code in accrual_codes:
|
||||
accounts = env['account.account'].search([
|
||||
('code', '=like', f'{code}%'),
|
||||
('company_id', '=', env.company.id),
|
||||
])
|
||||
for acct in 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 {'accruals': result}
|
||||
|
||||
|
||||
def run_hash_integrity_check(env, params):
|
||||
try:
|
||||
result = env.company._check_hash_integrity()
|
||||
return {
|
||||
'status': 'completed',
|
||||
'results': result.get('results', []),
|
||||
'printing_date': result.get('printing_date', ''),
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def get_period_summary(env, params):
|
||||
date_from = params.get('date_from')
|
||||
date_to = params.get('date_to')
|
||||
try:
|
||||
report = env.ref('account_reports.trial_balance_report')
|
||||
except Exception:
|
||||
report = env.ref('account.trial_balance_report', raise_if_not_found=False)
|
||||
if not report:
|
||||
return {'error': 'Trial balance report not found'}
|
||||
options = report.get_options({'date': {'date_from': date_from, 'date_to': date_to}})
|
||||
lines = report._get_lines(options)
|
||||
return {
|
||||
'period': f'{date_from} to {date_to}',
|
||||
'lines': [{
|
||||
'name': l.get('name', ''),
|
||||
'columns': [c.get('no_format', c.get('name', '')) for c in l.get('columns', [])],
|
||||
} for l in lines[:100]],
|
||||
}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_close_checklist': get_close_checklist,
|
||||
'get_unreconciled_counts': get_unreconciled_counts,
|
||||
'find_entries_in_locked_period': find_entries_in_locked_period,
|
||||
'get_accrual_status': get_accrual_status,
|
||||
'run_hash_integrity_check': run_hash_integrity_check,
|
||||
'get_period_summary': get_period_summary,
|
||||
}
|
||||
205
fusion_accounting/services/tools/payroll.py
Normal file
205
fusion_accounting/services/tools/payroll.py
Normal file
@@ -0,0 +1,205 @@
|
||||
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_id', '=', 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 create_payroll_journal_entry(env, params):
|
||||
journal_id = int(params['journal_id'])
|
||||
date = params['date']
|
||||
lines_data = params['lines']
|
||||
move_vals = {
|
||||
'journal_id': journal_id,
|
||||
'date': date,
|
||||
'ref': params.get('ref', 'Payroll Entry'),
|
||||
'line_ids': [(0, 0, {
|
||||
'account_id': int(line['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,
|
||||
}) for line in lines_data],
|
||||
}
|
||||
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,
|
||||
}
|
||||
117
fusion_accounting/services/tools/reporting.py
Normal file
117
fusion_accounting/services/tools/reporting.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import logging
|
||||
import base64
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_report(env, ref_id):
|
||||
try:
|
||||
return env.ref(ref_id)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _run_report(env, report_ref, params):
|
||||
report = _get_report(env, report_ref)
|
||||
if not report:
|
||||
return {'error': f'Report {report_ref} not found'}
|
||||
date_opts = {}
|
||||
if params.get('date_from'):
|
||||
date_opts['date_from'] = params['date_from']
|
||||
if params.get('date_to'):
|
||||
date_opts['date_to'] = params['date_to']
|
||||
options = report.get_options({'date': date_opts} if date_opts else {})
|
||||
lines = report._get_lines(options)
|
||||
return {
|
||||
'report_name': report.name,
|
||||
'lines': [{
|
||||
'name': l.get('name', ''),
|
||||
'level': l.get('level', 0),
|
||||
'columns': [c.get('no_format', c.get('name', '')) for c in l.get('columns', [])],
|
||||
} for l in lines[:100]],
|
||||
}
|
||||
|
||||
|
||||
def get_profit_loss(env, params):
|
||||
return _run_report(env, 'account_reports.profit_and_loss', params)
|
||||
|
||||
|
||||
def get_balance_sheet(env, params):
|
||||
return _run_report(env, 'account_reports.balance_sheet', params)
|
||||
|
||||
|
||||
def get_trial_balance(env, params):
|
||||
return _run_report(env, 'account_reports.trial_balance_report', params)
|
||||
|
||||
|
||||
def get_cash_flow(env, params):
|
||||
return _run_report(env, 'account_reports.cash_flow_statement', params)
|
||||
|
||||
|
||||
def compare_periods(env, params):
|
||||
report_ref = params.get('report_ref', 'account_reports.profit_and_loss')
|
||||
report = _get_report(env, report_ref)
|
||||
if not report:
|
||||
return {'error': f'Report {report_ref} not found'}
|
||||
|
||||
period1 = _run_report(env, report_ref, {
|
||||
'date_from': params.get('period1_from'),
|
||||
'date_to': params.get('period1_to'),
|
||||
})
|
||||
period2 = _run_report(env, report_ref, {
|
||||
'date_from': params.get('period2_from'),
|
||||
'date_to': params.get('period2_to'),
|
||||
})
|
||||
return {'period_1': period1, 'period_2': period2}
|
||||
|
||||
|
||||
def answer_financial_question(env, params):
|
||||
question = params.get('question', '')
|
||||
sql_query = params.get('sql_query')
|
||||
if sql_query:
|
||||
return {'error': 'Direct SQL not permitted. Use report tools instead.'}
|
||||
return {'status': 'info', 'message': f'Use specific report tools to answer: {question}'}
|
||||
|
||||
|
||||
def export_report(env, params):
|
||||
report_ref = params.get('report_ref', 'account_reports.profit_and_loss')
|
||||
fmt = params.get('format', 'pdf')
|
||||
report = _get_report(env, report_ref)
|
||||
if not report:
|
||||
return {'error': f'Report {report_ref} not found'}
|
||||
date_opts = {}
|
||||
if params.get('date_from'):
|
||||
date_opts['date_from'] = params['date_from']
|
||||
if params.get('date_to'):
|
||||
date_opts['date_to'] = params['date_to']
|
||||
options = report.get_options({'date': date_opts} if date_opts else {})
|
||||
|
||||
try:
|
||||
if fmt == 'xlsx':
|
||||
result = report.dispatch_report_action(options, 'export_to_xlsx')
|
||||
else:
|
||||
result = report.dispatch_report_action(options, 'export_to_pdf')
|
||||
|
||||
if isinstance(result, dict) and result.get('file_content'):
|
||||
return {
|
||||
'file_name': result.get('file_name', f'report.{fmt}'),
|
||||
'file_type': result.get('file_type', fmt),
|
||||
'file_content_b64': base64.b64encode(result['file_content']).decode(),
|
||||
}
|
||||
return {
|
||||
'status': 'generated',
|
||||
'message': f'Report exported as {fmt}. Use the Odoo UI to download.',
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': f'Export failed: {str(e)}'}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_profit_loss': get_profit_loss,
|
||||
'get_balance_sheet': get_balance_sheet,
|
||||
'get_trial_balance': get_trial_balance,
|
||||
'get_cash_flow': get_cash_flow,
|
||||
'compare_periods': compare_periods,
|
||||
'answer_financial_question': answer_financial_question,
|
||||
'export_report': export_report,
|
||||
}
|
||||
Reference in New Issue
Block a user