Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
# Fusion Accounting - Cash Basis Reporting
# Copyright (C) 2026 Nexa Systems Inc. (https://nexasystems.ca)
# Original implementation for the Fusion Accounting module.
#
# Alternative report handler that uses payment dates instead of invoice
# dates for recognizing revenue and expenses, supporting the cash basis
# accounting method.
import logging
from odoo import api, fields, models, _
from odoo.tools import SQL, Query, float_is_zero
_logger = logging.getLogger(__name__)
class FusionCashBasisReport(models.AbstractModel):
"""Cash basis report custom handler.
Unlike the standard accrual-based reporting, cash basis reports
recognise revenue when payment is received and expenses when payment
is made, regardless of when the invoice was issued.
This handler:
- Replaces the invoice/bill date with the payment reconciliation date
- Filters transactions to only include those with matching payments
- Provides a toggle in report options to switch between accrual and
cash basis views
"""
_name = 'account.cash.basis.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Cash Basis Report Custom Handler'
# ------------------------------------------------------------------
# Options Initializer
# ------------------------------------------------------------------
def _custom_options_initializer(self, report, options, previous_options):
"""Add cash-basis specific options to the report."""
super()._custom_options_initializer(report, options, previous_options=previous_options)
# Add the cash basis toggle
options['fusion_cash_basis'] = previous_options.get('fusion_cash_basis', True)
# Restrict to journals that support cash basis
report._init_options_journals(
options,
previous_options=previous_options,
additional_journals_domain=[('type', 'in', ('sale', 'purchase', 'bank', 'cash', 'general'))],
)
# ------------------------------------------------------------------
# Dynamic Lines
# ------------------------------------------------------------------
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
"""Generate report lines based on cash basis (payment date) accounting.
Returns a list of (sequence, line_dict) tuples for the report engine.
"""
output_lines = []
if not options.get('fusion_cash_basis', True):
# Fallback to standard accrual-based processing
return output_lines
cash_data = self._compute_cash_basis_data(report, options)
# ---- Revenue Section ----
revenue_total = sum(d['amount'] for d in cash_data.get('revenue', []))
output_lines.append((0, self._build_section_line(
report, options, 'revenue',
_("Cash Revenue"), revenue_total,
)))
for entry in sorted(cash_data.get('revenue', []), key=lambda e: e.get('date', '')):
output_lines.append((1, self._build_detail_line(report, options, entry)))
# ---- Expense Section ----
expense_total = sum(d['amount'] for d in cash_data.get('expense', []))
output_lines.append((0, self._build_section_line(
report, options, 'expense',
_("Cash Expenses"), expense_total,
)))
for entry in sorted(cash_data.get('expense', []), key=lambda e: e.get('date', '')):
output_lines.append((1, self._build_detail_line(report, options, entry)))
# ---- Net Cash Income ----
net_total = revenue_total - abs(expense_total)
output_lines.append((0, self._build_section_line(
report, options, 'net_income',
_("Net Cash Income"), net_total,
)))
return output_lines
# ------------------------------------------------------------------
# Data Computation
# ------------------------------------------------------------------
def _compute_cash_basis_data(self, report, options):
"""Compute cash basis amounts grouped by revenue/expense.
Queries reconciled payments to find the actual cash dates for
recognised amounts.
:returns: dict with keys ``revenue`` and ``expense``, each
containing a list of entry dicts with amount, date,
account, and partner information.
"""
result = {'revenue': [], 'expense': []}
company_ids = [c['id'] for c in options.get('companies', [{'id': self.env.company.id}])]
date_from = options.get('date', {}).get('date_from')
date_to = options.get('date', {}).get('date_to')
if not date_from or not date_to:
return result
# Query: find all payment reconciliation entries within the period
query = """
SELECT
aml.id AS line_id,
aml.account_id,
aa.name AS account_name,
aa.code AS account_code,
aml.partner_id,
rp.name AS partner_name,
apr.max_date AS cash_date,
CASE
WHEN aa.account_type IN ('income', 'income_other')
THEN aml.credit - aml.debit
ELSE aml.debit - aml.credit
END AS amount
FROM account_move_line aml
JOIN account_account aa ON aa.id = aml.account_id
LEFT JOIN res_partner rp ON rp.id = aml.partner_id
JOIN account_move am ON am.id = aml.move_id
JOIN (
SELECT
apr2.debit_move_id,
apr2.credit_move_id,
apr2.max_date
FROM account_partial_reconcile apr2
WHERE apr2.max_date >= %s
AND apr2.max_date <= %s
) apr ON (apr.debit_move_id = aml.id OR apr.credit_move_id = aml.id)
WHERE am.state = 'posted'
AND am.company_id IN %s
AND aa.account_type IN (
'income', 'income_other',
'expense', 'expense_direct_cost', 'expense_depreciation'
)
ORDER BY apr.max_date, aa.code
"""
self.env.cr.execute(query, (date_from, date_to, tuple(company_ids)))
rows = self.env.cr.dictfetchall()
seen_lines = set()
for row in rows:
# Avoid counting the same line twice if partially reconciled
if row['line_id'] in seen_lines:
continue
seen_lines.add(row['line_id'])
entry = {
'line_id': row['line_id'],
'account_id': row['account_id'],
'account_name': row['account_name'],
'account_code': row['account_code'] or '',
'partner_id': row['partner_id'],
'partner_name': row['partner_name'] or '',
'date': str(row['cash_date']),
'amount': row['amount'] or 0.0,
}
account_type = self.env['account.account'].browse(
row['account_id']
).account_type
if account_type in ('income', 'income_other'):
result['revenue'].append(entry)
else:
result['expense'].append(entry)
return result
# ------------------------------------------------------------------
# Line Builders
# ------------------------------------------------------------------
def _build_section_line(self, report, options, section_id, name, total):
"""Build a section header line for the report.
:param section_id: unique identifier for the section
:param name: display name of the section
:param total: aggregated monetary total
:returns: line dict compatible with the report engine
"""
columns = report._build_column_dict(total, options, figure_type='monetary')
return {
'id': report._get_generic_line_id(None, None, markup=f'fusion_cb_{section_id}'),
'name': name,
'level': 1,
'columns': [columns],
'unfoldable': False,
'unfolded': False,
}
def _build_detail_line(self, report, options, entry):
"""Build a detail line for a single cash-basis entry.
:param entry: dict with amount, account_code, account_name, etc.
:returns: line dict compatible with the report engine
"""
name = f"{entry['account_code']} {entry['account_name']}"
if entry.get('partner_name'):
name += f" - {entry['partner_name']}"
columns = report._build_column_dict(
entry['amount'], options, figure_type='monetary',
)
return {
'id': report._get_generic_line_id(
'account.move.line', entry['line_id'],
markup='fusion_cb_detail',
),
'name': name,
'level': 3,
'columns': [columns],
'caret_options': 'account.move.line',
'unfoldable': False,
}