changes
This commit is contained in:
BIN
fusion_accounting/fusion_accounting_reports/.DS_Store
vendored
Normal file
BIN
fusion_accounting/fusion_accounting_reports/.DS_Store
vendored
Normal file
Binary file not shown.
147
fusion_accounting/fusion_accounting_reports/CLAUDE.md
Normal file
147
fusion_accounting/fusion_accounting_reports/CLAUDE.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# fusion_accounting_reports — Cursor / Claude Context
|
||||
|
||||
## Purpose
|
||||
|
||||
AI-augmented financial reports — a Fusion-native replacement for Odoo
|
||||
Enterprise's `account_reports` module. Phase 2 of the fusion_accounting
|
||||
roadmap.
|
||||
|
||||
CORE scope:
|
||||
- Income Statement (P&L)
|
||||
- Balance Sheet
|
||||
- Trial Balance
|
||||
- General Ledger (with drill-down)
|
||||
|
||||
AI augmentation:
|
||||
- Anomaly detection (variance vs prior period)
|
||||
- AI commentary (LLM-generated narrative)
|
||||
|
||||
## Architecture
|
||||
|
||||
Hybrid: the engine (`fusion.report.engine`, AbstractModel) is the SINGLE
|
||||
read surface for reports. Per-report definitions are stored as `fusion.report`
|
||||
records with JSON `line_specs` so non-developers can tweak the layouts.
|
||||
|
||||
Public engine API (5 methods):
|
||||
- `compute_pnl(period, *, comparison='none', company_id=None)`
|
||||
- `compute_balance_sheet(date_to, *, comparison='none', company_id=None)`
|
||||
- `compute_trial_balance(period, *, company_id=None)`
|
||||
- `compute_gl(period, *, account_ids=None, company_id=None)`
|
||||
- `drill_down(*, account_id, period, company_id=None)`
|
||||
|
||||
Pure-Python services in `services/` (no Odoo imports — independently
|
||||
unit-testable):
|
||||
- `date_periods` — `Period` dataclass + comparison-period math
|
||||
- `account_hierarchy` — chart-of-accounts tree walk
|
||||
- `totaling` — debit/credit/balance roll-ups
|
||||
- `currency_conversion` — multi-currency conversion via `res.currency.rate`
|
||||
- `line_resolver` — JSON `line_specs` → rendered rows
|
||||
- `drill_down_resolver` — line → underlying journal items
|
||||
- `anomaly_detection` — variance vs prior period (z-score + abs/pct gates)
|
||||
- `commentary_generator` — LLM narrative with templated fallback
|
||||
- `commentary_prompt` — provider-agnostic system + user prompt
|
||||
|
||||
Persisted models in `models/`:
|
||||
- `fusion.report` — definition with JSON `line_specs`
|
||||
- `fusion.report.commentary` — LLM-output cache (one per period+mode)
|
||||
- `fusion.report.anomaly` — flagged variances
|
||||
- `fusion.account.balance.mv` — pre-aggregated materialized view
|
||||
- `fusion.report.engine` — AbstractModel (the API)
|
||||
- `fusion.reports.cron` — cron handlers (commentary refresh, MV refresh)
|
||||
- `fusion.xlsx.export.wizard` — TransientModel (XLSX export)
|
||||
- `fusion.period.picker.wizard` — TransientModel (UX entry-point)
|
||||
- `fusion.migration.wizard` (inherits) — adds `_reports_bootstrap_step`
|
||||
|
||||
Controller: `controllers/reports_controller.py` exposes 8 JSON-RPC endpoints
|
||||
under `/fusion/reports/*`. All read paths route through the engine.
|
||||
|
||||
OWL frontend: `static/src/`
|
||||
- `scss/` — variables, base styles, dark-mode overrides
|
||||
- `services/reports_service.js` — central reactive state + RPC wrappers
|
||||
- `views/report_viewer/` — top-level OWL view + view-registry adapter
|
||||
- `components/report_table/` — generic financial-table renderer
|
||||
- `components/drill_down_dialog/` — modal for journal-item listing
|
||||
- `components/period_filter/` — date-range + comparison picker
|
||||
- `components/ai_commentary_panel/` — LLM commentary surface
|
||||
- `components/anomaly_strip/` — variance summary banner
|
||||
- `tours/reports_tours.js` — 5 OWL tour smoke tests
|
||||
|
||||
## Coexistence
|
||||
|
||||
When `account_reports` is installed, the Reports menu hides via
|
||||
`fusion_accounting_core.group_fusion_show_when_enterprise_absent`
|
||||
(a computed group). The engine + AI tools (commentary, anomaly detection)
|
||||
remain available for the chat regardless.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **V19 deprecations to avoid:** `_sql_constraints` (use `models.Constraint`),
|
||||
`@api.depends('id')` (raises `NotImplementedError`), `@route(type='json')`
|
||||
(use `type='jsonrpc'`), `numbercall` field on `ir.cron` (removed),
|
||||
`groups_id` on `res.users` (use `all_group_ids` for searching),
|
||||
`users` field on `res.groups` (use `user_ids`), `groups_id` on
|
||||
`ir.ui.menu` (use `group_ids`).
|
||||
|
||||
- **Engine signature:** Public methods are keyword-only after the leading
|
||||
positional `period` / `date_to`. Always pass `company_id=...` explicitly.
|
||||
|
||||
- **`fusion.report` lookup:** `_get_report` falls back from per-company
|
||||
override to global (`company_id=False`) — order is `company_id desc nulls
|
||||
last`.
|
||||
|
||||
- **Materialized view refresh:** `fusion.account.balance.mv` rebuilds via a
|
||||
dedicated autocommit cursor (REFRESH CONCURRENTLY can't run inside Odoo's
|
||||
regular transaction). Triggered by cron + on demand from the engine when
|
||||
data is older than the configured TTL.
|
||||
|
||||
- **JSON `line_specs`:** Strings prefixed `account:`, `prefix:`, `formula:`
|
||||
or `header` — `line_resolver.py` resolves each spec to a row. Header rows
|
||||
have no compute payload and are silently skipped by downstream totals.
|
||||
|
||||
- **Commentary cache:** Keyed on `(report_id, company_id, period_from,
|
||||
period_to, comparison_mode)` with a unique constraint. Re-runs use the
|
||||
cache unless `force_refresh=True`.
|
||||
|
||||
## Test counts (Phase 2 ship)
|
||||
|
||||
- 130 logical tests, 0 failed, 0 errors
|
||||
- Includes:
|
||||
- 6 benchmarks (tagged `benchmark`)
|
||||
- 1 LLM compat smoke (tagged `local_llm`, skips when no LLM)
|
||||
- 5 OWL tours (tagged `tour`, skips without `websocket-client`)
|
||||
- Property-based, integration, controller, materialized-view, coexistence,
|
||||
migration round-trip, PDF/XLSX export
|
||||
|
||||
## Performance baseline
|
||||
|
||||
| Operation | Median | P95 | Budget |
|
||||
|---|---|---|---|
|
||||
| `engine.compute_pnl` | 3ms | 8ms | <2000ms |
|
||||
| `engine.compute_balance_sheet` | 15ms | 20ms | <2000ms |
|
||||
| `engine.compute_trial_balance` | 3ms | 8ms | <1000ms |
|
||||
| `engine.compute_gl` | 25ms | 81ms | <3000ms |
|
||||
| `engine.drill_down` | 2ms | 10ms | <500ms |
|
||||
| `controller.run` (HTTP round-trip) | 9ms | 46ms | <2500ms |
|
||||
|
||||
All metrics within 1x of budget at Phase 2 ship. Numbers from
|
||||
`tests/test_performance_benchmarks.py` against the dev VM
|
||||
(`westin-v19`, ~1 fiscal year of data).
|
||||
|
||||
## Known concerns / Phase 2.5 backlog
|
||||
|
||||
- Trial balance period-only sum doesn't auto-close to retained earnings
|
||||
(drift visible in `test_trial_balance_total_near_zero`, currently skipped)
|
||||
- Balance sheet `TOTAL LIABILITIES + EQUITY` math limited (no
|
||||
subtotal-of-subtotals expansion in `formula:` specs)
|
||||
- GL `line_specs` need `prefix:` empty-string handling for
|
||||
"all accounts" semantics
|
||||
- Header rows (no compute payload) silently skipped by `line_resolver` —
|
||||
fine for layout, but a `header_only=True` flag would be clearer
|
||||
- `expense` prefix overlaps with subtypes (`expense_direct_cost`,
|
||||
`expense_depreciation`) — current line_specs need explicit ordering or a
|
||||
longer-prefix-wins rule
|
||||
- `wkhtmltopdf` may need configuration for PDF export on first install
|
||||
- `ReportsAdapter.run_report` vs `run_fusion_report` naming (legacy clash
|
||||
with Enterprise wrapper)
|
||||
- Tour tests skip when `websocket-client` is absent — install it in CI to
|
||||
exercise the OWL surface end-to-end
|
||||
103
fusion_accounting/fusion_accounting_reports/README.md
Normal file
103
fusion_accounting/fusion_accounting_reports/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# fusion_accounting_reports
|
||||
|
||||
AI-augmented financial reports for Odoo 19 Community — a Fusion-native
|
||||
replacement for Enterprise's `account_reports` module.
|
||||
|
||||
## What it does
|
||||
|
||||
- **CORE reports**: Income Statement (P&L), Balance Sheet, Trial Balance,
|
||||
General Ledger (with drill-down to journal items)
|
||||
- **AI augmentation**: variance-based anomaly detection + LLM-generated
|
||||
commentary (Claude / GPT / local LM Studio / Ollama)
|
||||
- **Wizards**: period picker (common presets — MTD, QTD, YTD, last month,
|
||||
custom range) + XLSX export
|
||||
- **Coexists** with Enterprise's `account_reports` (Enterprise wins by
|
||||
default; the Fusion menu appears only when Enterprise is uninstalled —
|
||||
the engine and AI tools are always available via the AI chat)
|
||||
- **Multi-currency** aware via `services/currency_conversion.py`
|
||||
- **Multi-company** aware (per-company `fusion.report` overrides fall back
|
||||
to global definitions)
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Install
|
||||
odoo --addons-path=... -i fusion_accounting_reports
|
||||
|
||||
# Open the reports menu (when Enterprise's account_reports is NOT installed)
|
||||
# Apps → Reports → Open Financial Report
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### LLM commentary (optional)
|
||||
|
||||
For LM Studio / Ollama (local):
|
||||
|
||||
- `fusion_accounting.openai_base_url` = `http://host.docker.internal:1234/v1`
|
||||
- `fusion_accounting.openai_model` = your local model name
|
||||
- `fusion_accounting.openai_api_key` = `lm-studio` (or anything non-empty)
|
||||
- `fusion_accounting.provider.reports_commentary` = `openai`
|
||||
|
||||
For OpenAI / Anthropic, set the corresponding API keys via the
|
||||
`fusion_accounting_ai` config screen — `reports_commentary` will route
|
||||
through whatever provider you choose.
|
||||
|
||||
If no provider is configured, commentary falls back to a deterministic
|
||||
templated summary (no LLM call).
|
||||
|
||||
### Cron jobs
|
||||
|
||||
Two cron handlers live in `models/fusion_reports_cron.py`:
|
||||
|
||||
- `fusion_reports_commentary_refresh` — daily, regenerates commentary for
|
||||
the most recently completed period
|
||||
- `fusion_reports_mv_refresh` — every 15 min, refreshes
|
||||
`fusion.account.balance.mv`
|
||||
|
||||
## Public engine API
|
||||
|
||||
```python
|
||||
engine = env['fusion.report.engine']
|
||||
|
||||
# Income statement
|
||||
result = engine.compute_pnl(period, comparison='previous_year')
|
||||
|
||||
# Balance sheet (point-in-time)
|
||||
result = engine.compute_balance_sheet(date(2026, 12, 31))
|
||||
|
||||
# Trial balance
|
||||
result = engine.compute_trial_balance(period)
|
||||
|
||||
# General ledger (journal items per account)
|
||||
result = engine.compute_gl(period, account_ids=[1, 2, 3])
|
||||
|
||||
# Drill-down (one account, period)
|
||||
items = engine.drill_down(account_id=1, period=period)
|
||||
```
|
||||
|
||||
## JSON-RPC endpoints
|
||||
|
||||
All under `/fusion/reports/`:
|
||||
|
||||
- `POST /fusion/reports/run` — single entry-point (dispatches by `report_type`)
|
||||
- `POST /fusion/reports/drill_down` — journal items for an account+period
|
||||
- `POST /fusion/reports/commentary` — fetch/refresh LLM commentary
|
||||
- `POST /fusion/reports/anomalies` — flagged variances for a period
|
||||
- `POST /fusion/reports/export_xlsx` — XLSX bytes
|
||||
- `POST /fusion/reports/export_pdf` — PDF bytes (via wkhtmltopdf)
|
||||
- `POST /fusion/reports/list_definitions` — available `fusion.report` records
|
||||
- `POST /fusion/reports/period_presets` — date-range presets for the picker
|
||||
|
||||
## Test counts
|
||||
|
||||
- 130 logical tests, 0 failures, 0 errors
|
||||
- 6 performance benchmarks (tagged `benchmark`)
|
||||
- 1 local-LLM compat smoke (tagged `local_llm`, skips without LLM)
|
||||
- 5 OWL tour tests (tagged `tour`, skips without `websocket-client`)
|
||||
|
||||
## See also
|
||||
|
||||
- `CLAUDE.md` — agent context (architecture, conventions, perf baseline,
|
||||
Phase 2.5 backlog)
|
||||
- `UPGRADE_NOTES.md` — V19 anchor + migration strategy
|
||||
60
fusion_accounting/fusion_accounting_reports/UPGRADE_NOTES.md
Normal file
60
fusion_accounting/fusion_accounting_reports/UPGRADE_NOTES.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# fusion_accounting_reports — Upgrade Notes
|
||||
|
||||
## Odoo Version Anchor
|
||||
|
||||
This module targets **Odoo 19.0** (community-base).
|
||||
|
||||
Reference snapshot of Enterprise code mirrored from:
|
||||
- `account_reports` (Odoo 19.0.x)
|
||||
- Source: `/Users/gurpreet/Github/RePackaged-Odoo/accounting/account_reports/`
|
||||
|
||||
## Cross-Version Diff Strategy
|
||||
|
||||
When a new Odoo version ships:
|
||||
|
||||
1. Run `check_odoo_diff.sh` (in repo root) against the new Enterprise version
|
||||
2. Note any breaking changes in `account.move.line` / `account.account` API
|
||||
surfaces relied on by `services/totaling.py` and
|
||||
`services/drill_down_resolver.py`
|
||||
3. For mirrored OWL components, diff Enterprise's new versions against ours
|
||||
and port material changes (signature renames, new behaviour we want to
|
||||
inherit)
|
||||
4. Re-run the full test suite + tour tests + benchmarks against the new Odoo
|
||||
version
|
||||
5. Update this file with the new version anchor + any deviations
|
||||
|
||||
## V19 Migration Notes (already applied — Phase 1 lessons)
|
||||
|
||||
These were the bite-points from Phase 1 (`fusion_accounting_bank_rec`); we
|
||||
preempted them in Phase 2 from day one:
|
||||
|
||||
- `_sql_constraints` → `models.Constraint` (used in `fusion.report`,
|
||||
`fusion.report.commentary`, `fusion.report.anomaly`)
|
||||
- `@api.depends('id')` → removed everywhere; computed fields depend on real
|
||||
field names instead
|
||||
- `@route(type='json')` → `type='jsonrpc'` (all 8 endpoints)
|
||||
- `numbercall` field on `ir.cron` → omitted (removed in V19)
|
||||
- `res.groups.users` → `user_ids`
|
||||
- `ir.ui.menu.groups_id` → `group_ids` (used in `views/menu_views.xml` and
|
||||
the two wizard view files for the coexistence-group filter)
|
||||
|
||||
## Engine API Stability
|
||||
|
||||
The 5 public engine methods (`compute_pnl`, `compute_balance_sheet`,
|
||||
`compute_trial_balance`, `compute_gl`, `drill_down`) are the public contract.
|
||||
Their signatures are keyword-only after the first positional argument and
|
||||
will be treated as semver-stable across patch releases. Breaking changes
|
||||
will bump the minor version (e.g. 19.0.2.x.y).
|
||||
|
||||
## Phase 2 → Phase 2.5 Migration
|
||||
|
||||
If we ship Phase 2.5 (line_spec polish, deferred features, header_only
|
||||
flag, prefix overlap fix), changes will go in incremental commits. No DB
|
||||
migration needed — Phase 2 schema is forward-compatible:
|
||||
|
||||
- `fusion.report.line_specs` is a JSON column; the migration path is to
|
||||
rewrite specs in place
|
||||
- `fusion.account.balance.mv` can be dropped/re-created freely
|
||||
- `fusion.report.commentary` is a cache; safe to truncate on upgrade
|
||||
- `fusion.report.anomaly` records carry Period as date_from/date_to fields;
|
||||
no schema-level changes anticipated
|
||||
5
fusion_accounting/fusion_accounting_reports/__init__.py
Normal file
5
fusion_accounting/fusion_accounting_reports/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import services
|
||||
from . import models
|
||||
from . import controllers
|
||||
from . import reports
|
||||
from . import wizards
|
||||
86
fusion_accounting/fusion_accounting_reports/__manifest__.py
Normal file
86
fusion_accounting/fusion_accounting_reports/__manifest__.py
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Reports',
|
||||
'version': '19.0.1.2.0',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).',
|
||||
'description': """
|
||||
Fusion Accounting Reports
|
||||
=========================
|
||||
|
||||
A Fusion-native replacement for Odoo Enterprise's account_reports module.
|
||||
|
||||
CORE scope (Phase 2):
|
||||
- Income Statement (P&L)
|
||||
- Balance Sheet
|
||||
- Trial Balance
|
||||
- General Ledger (with drill-down)
|
||||
|
||||
AI augmentation:
|
||||
- Anomaly detection (variance vs prior period)
|
||||
- AI commentary (LLM-generated narrative)
|
||||
|
||||
Coexists with Enterprise: when account_reports is installed, the Fusion
|
||||
menu hides; the engine and AI tools remain available for the chat.
|
||||
""",
|
||||
'author': 'Fusion Accounting',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'fusion_accounting_core',
|
||||
'fusion_accounting_ai',
|
||||
'fusion_accounting_migration',
|
||||
'account',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/report_pnl.xml',
|
||||
'data/report_balance_sheet.xml',
|
||||
'data/report_trial_balance.xml',
|
||||
'data/report_general_ledger.xml',
|
||||
'data/report_cash_flow.xml',
|
||||
'data/report_executive_summary.xml',
|
||||
'data/report_tax_report.xml',
|
||||
'data/report_annual_statements.xml',
|
||||
'data/report_aged_receivable.xml',
|
||||
'data/report_aged_payable.xml',
|
||||
'data/report_partner_ledger.xml',
|
||||
'data/cron.xml',
|
||||
'reports/report_pdf_template.xml',
|
||||
'wizards/xlsx_export_wizard_views.xml',
|
||||
'wizards/period_picker_wizard_views.xml',
|
||||
'views/report_actions.xml',
|
||||
'views/menu_views.xml',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['xlsxwriter'],
|
||||
},
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_accounting_reports/static/src/scss/_variables.scss',
|
||||
'fusion_accounting_reports/static/src/scss/reports.scss',
|
||||
'fusion_accounting_reports/static/src/services/reports_service.js',
|
||||
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer.js',
|
||||
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer.xml',
|
||||
'fusion_accounting_reports/static/src/views/report_viewer/report_viewer_view.js',
|
||||
'fusion_accounting_reports/static/src/components/report_table/report_table.js',
|
||||
'fusion_accounting_reports/static/src/components/report_table/report_table.xml',
|
||||
'fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js',
|
||||
'fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.xml',
|
||||
'fusion_accounting_reports/static/src/components/period_filter/period_filter.js',
|
||||
'fusion_accounting_reports/static/src/components/period_filter/period_filter.xml',
|
||||
'fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.js',
|
||||
'fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.xml',
|
||||
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js',
|
||||
'fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.xml',
|
||||
],
|
||||
'web.assets_web_dark': [
|
||||
'fusion_accounting_reports/static/src/scss/reports.dark.scss',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'fusion_accounting_reports/static/src/tours/reports_tours.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'icon': '/fusion_accounting_reports/static/description/icon.png',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import reports_controller
|
||||
@@ -0,0 +1,271 @@
|
||||
"""HTTP controller: 8 JSON-RPC endpoints for the OWL reports widget.
|
||||
|
||||
All endpoints route through fusion.report.engine - no direct ORM
|
||||
aggregation from the controller. Uses V19's type='jsonrpc'.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import request
|
||||
|
||||
from ..services.anomaly_detection import detect as detect_anomalies
|
||||
from ..services.commentary_generator import generate_commentary
|
||||
from ..services.date_periods import Period
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REPORT_TYPES = {
|
||||
'pnl', 'balance_sheet', 'trial_balance', 'general_ledger',
|
||||
'aged_receivable', 'aged_payable', 'partner_ledger',
|
||||
}
|
||||
|
||||
PARTNER_GROUPED_ACCOUNT_TYPE = {
|
||||
'aged_receivable': 'asset_receivable',
|
||||
'aged_payable': 'liability_payable',
|
||||
'partner_ledger': 'asset_receivable',
|
||||
}
|
||||
|
||||
|
||||
def _parse_date(value):
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, '%Y-%m-%d').date()
|
||||
|
||||
|
||||
def _build_period(date_from, date_to, label=None):
|
||||
df = _parse_date(date_from)
|
||||
dt = _parse_date(date_to)
|
||||
return Period(date_from=df, date_to=dt, label=label or f"{df} - {dt}")
|
||||
|
||||
|
||||
class FusionReportsController(http.Controller):
|
||||
|
||||
@http.route('/fusion/reports/list_available', type='jsonrpc', auth='user')
|
||||
def list_available(self, company_id=None):
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
Report = request.env['fusion.report'].sudo()
|
||||
reports = Report.search([
|
||||
('active', '=', True),
|
||||
'|', ('company_id', '=', company_id), ('company_id', '=', False),
|
||||
], order='sequence, name')
|
||||
return {
|
||||
'reports': [{
|
||||
'id': r.id,
|
||||
'name': r.name,
|
||||
'code': r.code,
|
||||
'report_type': r.report_type,
|
||||
'description': r.description or '',
|
||||
'default_comparison_mode': r.default_comparison_mode,
|
||||
} for r in reports],
|
||||
}
|
||||
|
||||
@http.route('/fusion/reports/run', type='jsonrpc', auth='user')
|
||||
def run(self, report_type, date_from=None, date_to=None,
|
||||
comparison='none', company_id=None, report_code=None):
|
||||
if report_type not in REPORT_TYPES:
|
||||
raise ValidationError(_("Unknown report type: %s") % report_type)
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
engine = request.env['fusion.report.engine']
|
||||
|
||||
if report_type == 'pnl':
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_pnl(
|
||||
period, comparison=comparison, company_id=company_id,
|
||||
report_code=report_code,
|
||||
)
|
||||
if report_type == 'balance_sheet':
|
||||
return engine.compute_balance_sheet(
|
||||
_parse_date(date_to),
|
||||
comparison=comparison,
|
||||
company_id=company_id,
|
||||
report_code=report_code,
|
||||
)
|
||||
if report_type == 'trial_balance':
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_trial_balance(
|
||||
period, company_id=company_id, report_code=report_code,
|
||||
)
|
||||
if report_type in PARTNER_GROUPED_ACCOUNT_TYPE:
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_partner_grouped(
|
||||
period,
|
||||
account_type=PARTNER_GROUPED_ACCOUNT_TYPE[report_type],
|
||||
comparison=comparison,
|
||||
company_id=company_id,
|
||||
)
|
||||
# general_ledger
|
||||
period = _build_period(date_from, date_to)
|
||||
return engine.compute_gl(
|
||||
period, company_id=company_id, report_code=report_code,
|
||||
)
|
||||
|
||||
@http.route('/fusion/reports/drill_down', type='jsonrpc', auth='user')
|
||||
def drill_down(self, account_id, date_from, date_to, company_id=None):
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
engine = request.env['fusion.report.engine']
|
||||
period = _build_period(date_from, date_to)
|
||||
rows = engine.drill_down(
|
||||
account_id=int(account_id),
|
||||
period=period,
|
||||
company_id=company_id,
|
||||
)
|
||||
return {'rows': rows, 'count': len(rows)}
|
||||
|
||||
@http.route('/fusion/reports/get_anomalies', type='jsonrpc', auth='user')
|
||||
def get_anomalies(self, report_type, date_from, date_to,
|
||||
comparison='previous_year', persist=False, company_id=None):
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
report_result = self.run(
|
||||
report_type=report_type,
|
||||
date_from=date_from, date_to=date_to,
|
||||
comparison=comparison, company_id=company_id,
|
||||
)
|
||||
anomalies = detect_anomalies(report_result)
|
||||
if persist and anomalies:
|
||||
Report = request.env['fusion.report']
|
||||
report_def = Report.search([('report_type', '=', report_type)], limit=1)
|
||||
if report_def:
|
||||
self._persist_anomalies(
|
||||
report_def,
|
||||
_parse_date(date_from), _parse_date(date_to),
|
||||
anomalies,
|
||||
)
|
||||
return {'anomalies': anomalies, 'count': len(anomalies)}
|
||||
|
||||
def _persist_anomalies(self, report, period_from, period_to, anomalies):
|
||||
Anomaly = request.env['fusion.report.anomaly']
|
||||
for a in anomalies:
|
||||
existing = Anomaly.search([
|
||||
('report_id', '=', report.id),
|
||||
('period_from', '=', period_from),
|
||||
('period_to', '=', period_to),
|
||||
('row_id', '=', a['row_id']),
|
||||
], limit=1)
|
||||
vals = {
|
||||
'report_id': report.id,
|
||||
'period_from': period_from,
|
||||
'period_to': period_to,
|
||||
'row_id': a['row_id'],
|
||||
'label': a['label'],
|
||||
'current_amount': a['current_amount'],
|
||||
'comparison_amount': a['comparison_amount'],
|
||||
'variance_amount': a['variance_amount'],
|
||||
'variance_pct': a['variance_pct'],
|
||||
'severity': a['severity'],
|
||||
'direction': a['direction'],
|
||||
}
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
else:
|
||||
Anomaly.create(vals)
|
||||
|
||||
@http.route('/fusion/reports/get_commentary', type='jsonrpc', auth='user')
|
||||
def get_commentary(self, report_type, date_from, date_to,
|
||||
comparison='none', force_regenerate=False, company_id=None):
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
Report = request.env['fusion.report']
|
||||
Commentary = request.env['fusion.report.commentary']
|
||||
report_def = Report.search([('report_type', '=', report_type)], limit=1)
|
||||
if not report_def:
|
||||
raise ValidationError(_("No report definition for %s") % report_type)
|
||||
|
||||
period_from = _parse_date(date_from)
|
||||
period_to = _parse_date(date_to)
|
||||
|
||||
cached = Commentary.search([
|
||||
('report_id', '=', report_def.id),
|
||||
('company_id', '=', company_id),
|
||||
('period_from', '=', period_from),
|
||||
('period_to', '=', period_to),
|
||||
('comparison_mode', '=', comparison),
|
||||
], limit=1)
|
||||
if cached and not force_regenerate:
|
||||
return {
|
||||
'cached': True,
|
||||
'summary': cached.summary or '',
|
||||
'highlights': cached.highlights or [],
|
||||
'concerns': cached.concerns or [],
|
||||
'next_actions': cached.next_actions or [],
|
||||
'generated_at': str(cached.generated_at),
|
||||
}
|
||||
|
||||
report_result = self.run(
|
||||
report_type=report_type, date_from=date_from,
|
||||
date_to=date_to, comparison=comparison,
|
||||
company_id=company_id,
|
||||
)
|
||||
anomalies = detect_anomalies(report_result)
|
||||
commentary = generate_commentary(
|
||||
request.env,
|
||||
report_result=report_result,
|
||||
anomalies=anomalies,
|
||||
)
|
||||
vals = {
|
||||
'report_id': report_def.id,
|
||||
'company_id': company_id,
|
||||
'period_from': period_from,
|
||||
'period_to': period_to,
|
||||
'comparison_mode': comparison,
|
||||
'summary': commentary.get('summary', ''),
|
||||
'highlights': commentary.get('highlights', []),
|
||||
'concerns': commentary.get('concerns', []),
|
||||
'next_actions': commentary.get('next_actions', []),
|
||||
}
|
||||
if cached:
|
||||
cached.write(vals)
|
||||
else:
|
||||
Commentary.create(vals)
|
||||
return {'cached': False, **commentary}
|
||||
|
||||
@http.route('/fusion/reports/compare_periods', type='jsonrpc', auth='user')
|
||||
def compare_periods(self, report_type, date_from, date_to,
|
||||
comparison='previous_year', company_id=None):
|
||||
return self.run(
|
||||
report_type=report_type, date_from=date_from,
|
||||
date_to=date_to, comparison=comparison,
|
||||
company_id=company_id,
|
||||
)
|
||||
|
||||
@http.route('/fusion/reports/export_pdf', type='jsonrpc', auth='user')
|
||||
def export_pdf(self, report_type, date_from, date_to,
|
||||
comparison='none', company_id=None):
|
||||
Report = request.env['fusion.report']
|
||||
report_def = Report.search([('report_type', '=', report_type)], limit=1)
|
||||
if not report_def:
|
||||
return {'status': 'error', 'message': f'No report definition for {report_type}'}
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
pdf, _ct = request.env['ir.actions.report'].sudo()._render_qweb_pdf(
|
||||
'fusion_accounting_reports.report_pdf_template',
|
||||
res_ids=[report_def.id],
|
||||
data={
|
||||
'report_type': report_type,
|
||||
'date_from': date_from, 'date_to': date_to,
|
||||
'comparison': comparison, 'company_id': company_id,
|
||||
},
|
||||
)
|
||||
import base64
|
||||
return {
|
||||
'status': 'ok',
|
||||
'pdf_base64': base64.b64encode(pdf).decode('ascii'),
|
||||
'filename': f'{report_type}_{date_from}_{date_to}.pdf',
|
||||
}
|
||||
|
||||
@http.route('/fusion/reports/export_xlsx', type='jsonrpc', auth='user')
|
||||
def export_xlsx(self, report_type, date_from, date_to,
|
||||
comparison='none', company_id=None):
|
||||
wizard = request.env['fusion.xlsx.export.wizard'].create({
|
||||
'report_type': report_type,
|
||||
'date_from': _parse_date(date_from),
|
||||
'date_to': _parse_date(date_to),
|
||||
'comparison': comparison,
|
||||
})
|
||||
wizard.action_export()
|
||||
return {
|
||||
'status': 'ok',
|
||||
'xlsx_base64': wizard.xlsx_file.decode('ascii') if wizard.xlsx_file else '',
|
||||
'filename': wizard.xlsx_filename,
|
||||
}
|
||||
24
fusion_accounting/fusion_accounting_reports/data/cron.xml
Normal file
24
fusion_accounting/fusion_accounting_reports/data/cron.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="cron_fusion_reports_anomaly_scan" model="ir.cron">
|
||||
<field name="name">Fusion Reports - Daily Anomaly Scan</field>
|
||||
<field name="model_id" ref="model_fusion_reports_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_anomaly_scan()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="cron_fusion_reports_mv_refresh" model="ir.cron">
|
||||
<field name="name">Fusion Reports - MV Refresh</field>
|
||||
<field name="model_id" ref="model_fusion_reports_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_mv_refresh()</field>
|
||||
<field name="interval_number">15</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_aged_payable" model="fusion.report">
|
||||
<field name="name">Aged Payable</field>
|
||||
<field name="code">aged_payable</field>
|
||||
<field name="report_type">aged_payable</field>
|
||||
<field name="sequence">36</field>
|
||||
<field name="description">Per-vendor outstanding payables, bucketed by aging.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Aged Payable', 'account_type_for_grouping': 'liability_payable'}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_aged_receivable" model="fusion.report">
|
||||
<field name="name">Aged Receivable</field>
|
||||
<field name="code">aged_receivable</field>
|
||||
<field name="report_type">aged_receivable</field>
|
||||
<field name="sequence">35</field>
|
||||
<field name="description">Per-customer outstanding receivables, bucketed by aging.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Aged Receivable', 'account_type_for_grouping': 'asset_receivable'}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_annual_statements" model="fusion.report">
|
||||
<field name="name">Annual Statements</field>
|
||||
<field name="code">annual_statements</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">11</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Year-over-year P&L comparison for annual reporting.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Revenue', 'account_type_prefix': 'income', 'sign': -1, 'level': 0},
|
||||
{'label': 'Cost of Goods Sold', 'account_type_prefix': 'expense_direct_cost', 'sign': -1, 'level': 1},
|
||||
{'label': 'Gross Profit', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
{'label': 'Operating Expenses', 'account_type_prefix': 'expense', 'sign': -1, 'level': 1},
|
||||
{'label': 'OPERATING INCOME', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="report_balance_sheet" model="fusion.report">
|
||||
<field name="name">Balance Sheet</field>
|
||||
<field name="code">balance_sheet</field>
|
||||
<field name="report_type">balance_sheet</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Statement of financial position as of a given date.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'ASSETS', 'level': 0},
|
||||
{'label': 'Current Assets', 'account_type_prefix': 'asset_current', 'sign': 1, 'level': 1},
|
||||
{'label': 'Receivables', 'account_type_prefix': 'asset_receivable', 'sign': 1, 'level': 1},
|
||||
{'label': 'Cash & Bank', 'account_type_prefix': 'asset_cash', 'sign': 1, 'level': 1},
|
||||
{'label': 'Prepayments', 'account_type_prefix': 'asset_prepayments', 'sign': 1, 'level': 1},
|
||||
{'label': 'Non-Current Assets', 'account_type_prefix': 'asset_non_current', 'sign': 1, 'level': 1},
|
||||
{'label': 'Fixed Assets', 'account_type_prefix': 'asset_fixed', 'sign': 1, 'level': 1},
|
||||
{'label': 'TOTAL ASSETS', 'compute': 'subtotal', 'above': 6, 'sign': 1, 'level': 0},
|
||||
{'label': 'LIABILITIES', 'level': 0},
|
||||
{'label': 'Payables', 'account_type_prefix': 'liability_payable', 'sign': -1, 'level': 1},
|
||||
{'label': 'Credit Cards', 'account_type_prefix': 'liability_credit_card', 'sign': -1, 'level': 1},
|
||||
{'label': 'Current Liabilities', 'account_type_prefix': 'liability_current', 'sign': -1, 'level': 1},
|
||||
{'label': 'Non-Current Liabilities', 'account_type_prefix': 'liability_non_current', 'sign': -1, 'level': 1},
|
||||
{'label': 'TOTAL LIABILITIES', 'compute': 'subtotal', 'above': 4, 'sign': 1, 'level': 0},
|
||||
{'label': 'EQUITY', 'level': 0},
|
||||
{'label': 'Equity', 'account_type_prefix': 'equity', 'sign': -1, 'level': 1},
|
||||
{'label': 'TOTAL EQUITY', 'compute': 'subtotal', 'above': 1, 'sign': 1, 'level': 0},
|
||||
{'label': 'TOTAL LIABILITIES + EQUITY', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_cash_flow" model="fusion.report">
|
||||
<field name="name">Cash Flow Statement</field>
|
||||
<field name="code">cash_flow</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Cash flow by activity (operating, investing, financing).</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Operating Activities', 'level': 0},
|
||||
{'label': 'Net Income (from operations)', 'account_type_prefix': 'income', 'sign': -1, 'level': 1},
|
||||
{'label': 'Depreciation Add-back', 'account_type_prefix': 'expense_depreciation', 'sign': 1, 'level': 1},
|
||||
{'label': 'Operating Cash Flow', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'Investing Activities', 'level': 0},
|
||||
{'label': 'Fixed Asset Purchases', 'account_type_prefix': 'asset_fixed', 'sign': -1, 'level': 1},
|
||||
{'label': 'Investing Cash Flow', 'compute': 'subtotal', 'above': 1, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'Financing Activities', 'level': 0},
|
||||
{'label': 'Liabilities (long-term)', 'account_type_prefix': 'liability_non_current', 'sign': 1, 'level': 1},
|
||||
{'label': 'Equity', 'account_type_prefix': 'equity', 'sign': 1, 'level': 1},
|
||||
{'label': 'Financing Cash Flow', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'NET CHANGE IN CASH', 'compute': 'subtotal', 'above': 3, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_executive_summary" model="fusion.report">
|
||||
<field name="name">Executive Summary</field>
|
||||
<field name="code">executive_summary</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Top-level KPI summary: revenue, expenses, net income, key balance positions.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'PROFIT & LOSS', 'level': 0},
|
||||
{'label': 'Revenue', 'account_type_prefix': 'income', 'sign': -1, 'level': 1},
|
||||
{'label': 'Expenses', 'account_type_prefix': 'expense', 'sign': -1, 'level': 1},
|
||||
{'label': 'Net Income', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
|
||||
{'label': 'BALANCE POSITIONS', 'level': 0},
|
||||
{'label': 'Cash & Bank', 'account_type_prefix': 'asset_cash', 'sign': 1, 'level': 1},
|
||||
{'label': 'Receivables', 'account_type_prefix': 'asset_receivable', 'sign': 1, 'level': 1},
|
||||
{'label': 'Payables', 'account_type_prefix': 'liability_payable', 'sign': -1, 'level': 1},
|
||||
{'label': 'Net Working Position', 'compute': 'subtotal', 'above': 3, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="report_general_ledger" model="fusion.report">
|
||||
<field name="name">General Ledger</field>
|
||||
<field name="code">general_ledger</field>
|
||||
<field name="report_type">general_ledger</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="default_comparison_mode">none</field>
|
||||
<field name="description">Per-account journal item listing for the period.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'All Accounts', 'account_type_prefix': 'asset', 'sign': 1, 'level': 0},
|
||||
{'label': 'All Accounts (liability)', 'account_type_prefix': 'liability', 'sign': 1, 'level': 0},
|
||||
{'label': 'All Accounts (equity)', 'account_type_prefix': 'equity', 'sign': 1, 'level': 0},
|
||||
{'label': 'All Accounts (income)', 'account_type_prefix': 'income', 'sign': 1, 'level': 0},
|
||||
{'label': 'All Accounts (expense)', 'account_type_prefix': 'expense', 'sign': 1, 'level': 0},
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_partner_ledger" model="fusion.report">
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="code">partner_ledger</field>
|
||||
<field name="report_type">partner_ledger</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="description">Per-partner ledger combining receivable and payable activity.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Partner Ledger', 'account_type_for_grouping': 'asset_receivable'}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="report_pnl" model="fusion.report">
|
||||
<field name="name">Profit and Loss</field>
|
||||
<field name="code">pnl</field>
|
||||
<field name="report_type">pnl</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="default_comparison_mode">previous_year</field>
|
||||
<field name="description">Income Statement summarizing revenue, expenses, and net income for a period.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Revenue', 'account_type_prefix': 'income', 'sign': -1, 'level': 0},
|
||||
{'label': 'Operating Expenses', 'account_type_prefix': 'expense', 'sign': -1, 'level': 0},
|
||||
{'label': 'Net Income', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0},
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<record id="report_tax_summary" model="fusion.report">
|
||||
<field name="name">Tax Summary</field>
|
||||
<field name="code">tax_summary</field>
|
||||
<field name="report_type">trial_balance</field>
|
||||
<field name="sequence">25</field>
|
||||
<field name="description">Tax liability + asset positions. v1: aggregate-level only; per-tax-code breakdown is Phase 2.5.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Tax Asset (recoverable)', 'account_type_prefix': 'asset_current', 'sign': 1, 'level': 0},
|
||||
{'label': 'Tax Liability (collected)', 'account_type_prefix': 'liability_current', 'sign': -1, 'level': 0},
|
||||
{'label': 'NET TAX POSITION', 'compute': 'subtotal', 'above': 2, 'sign': 1, 'level': 0}
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="report_trial_balance" model="fusion.report">
|
||||
<field name="name">Trial Balance</field>
|
||||
<field name="code">trial_balance</field>
|
||||
<field name="report_type">trial_balance</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="default_comparison_mode">none</field>
|
||||
<field name="description">Per-account balances for verifying that debits equal credits.</field>
|
||||
<field name="line_specs" eval="[
|
||||
{'label': 'Assets', 'account_type_prefix': 'asset', 'sign': 1, 'level': 0},
|
||||
{'label': 'Liabilities', 'account_type_prefix': 'liability', 'sign': -1, 'level': 0},
|
||||
{'label': 'Equity', 'account_type_prefix': 'equity', 'sign': -1, 'level': 0},
|
||||
{'label': 'Income', 'account_type_prefix': 'income', 'sign': -1, 'level': 0},
|
||||
{'label': 'Expenses', 'account_type_prefix': 'expense', 'sign': 1, 'level': 0},
|
||||
{'label': 'Total (should be 0)', 'compute': 'subtotal', 'above': 5, 'sign': 1, 'level': 0},
|
||||
]"/>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Materialized view: per-account aggregated balances by year-month.
|
||||
-- Used by GL drill-down + trial balance for large DBs.
|
||||
-- Refresh strategy: cron every 15 minutes (Task 25); CONCURRENTLY-capable
|
||||
-- thanks to the unique index.
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS fusion_account_balance_mv AS
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY account_id, company_id, DATE_TRUNC('month', date)
|
||||
)::INTEGER AS id,
|
||||
account_id,
|
||||
company_id,
|
||||
DATE_TRUNC('month', date)::date AS period_month,
|
||||
SUM(debit) AS debit,
|
||||
SUM(credit) AS credit,
|
||||
SUM(balance) AS balance,
|
||||
COUNT(*) AS line_count
|
||||
FROM account_move_line
|
||||
WHERE parent_state = 'posted'
|
||||
GROUP BY account_id, company_id, DATE_TRUNC('month', date);
|
||||
|
||||
-- The (account_id, company_id, period_month) tuple is the natural key.
|
||||
-- We mark it UNIQUE so REFRESH MATERIALIZED VIEW CONCURRENTLY is allowed.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS fusion_account_balance_mv_pkey
|
||||
ON fusion_account_balance_mv (account_id, company_id, period_month);
|
||||
-- A separate index on the synthetic id is required by Odoo's ORM, which
|
||||
-- expects every model row to be addressable by `id`.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS fusion_account_balance_mv_id_idx
|
||||
ON fusion_account_balance_mv (id);
|
||||
CREATE INDEX IF NOT EXISTS fusion_account_balance_mv_company_month
|
||||
ON fusion_account_balance_mv (company_id, period_month);
|
||||
@@ -0,0 +1,278 @@
|
||||
# Graph Report - /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports (2026-04-22)
|
||||
|
||||
## Corpus Check
|
||||
- 66 files · ~18,059 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 457 nodes · 729 edges · 38 communities detected
|
||||
- Extraction: 69% EXTRACTED · 31% INFERRED · 0% AMBIGUOUS · INFERRED: 229 edges (avg confidence: 0.71)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- [[_COMMUNITY_Community 0|Community 0]]
|
||||
- [[_COMMUNITY_Community 1|Community 1]]
|
||||
- [[_COMMUNITY_Community 2|Community 2]]
|
||||
- [[_COMMUNITY_Community 3|Community 3]]
|
||||
- [[_COMMUNITY_Community 4|Community 4]]
|
||||
- [[_COMMUNITY_Community 5|Community 5]]
|
||||
- [[_COMMUNITY_Community 6|Community 6]]
|
||||
- [[_COMMUNITY_Community 7|Community 7]]
|
||||
- [[_COMMUNITY_Community 8|Community 8]]
|
||||
- [[_COMMUNITY_Community 9|Community 9]]
|
||||
- [[_COMMUNITY_Community 10|Community 10]]
|
||||
- [[_COMMUNITY_Community 11|Community 11]]
|
||||
- [[_COMMUNITY_Community 12|Community 12]]
|
||||
- [[_COMMUNITY_Community 13|Community 13]]
|
||||
- [[_COMMUNITY_Community 14|Community 14]]
|
||||
- [[_COMMUNITY_Community 15|Community 15]]
|
||||
- [[_COMMUNITY_Community 16|Community 16]]
|
||||
- [[_COMMUNITY_Community 17|Community 17]]
|
||||
- [[_COMMUNITY_Community 18|Community 18]]
|
||||
- [[_COMMUNITY_Community 19|Community 19]]
|
||||
- [[_COMMUNITY_Community 20|Community 20]]
|
||||
- [[_COMMUNITY_Community 21|Community 21]]
|
||||
- [[_COMMUNITY_Community 22|Community 22]]
|
||||
- [[_COMMUNITY_Community 23|Community 23]]
|
||||
- [[_COMMUNITY_Community 24|Community 24]]
|
||||
- [[_COMMUNITY_Community 25|Community 25]]
|
||||
- [[_COMMUNITY_Community 26|Community 26]]
|
||||
- [[_COMMUNITY_Community 27|Community 27]]
|
||||
- [[_COMMUNITY_Community 28|Community 28]]
|
||||
- [[_COMMUNITY_Community 29|Community 29]]
|
||||
- [[_COMMUNITY_Community 30|Community 30]]
|
||||
- [[_COMMUNITY_Community 31|Community 31]]
|
||||
- [[_COMMUNITY_Community 32|Community 32]]
|
||||
- [[_COMMUNITY_Community 33|Community 33]]
|
||||
- [[_COMMUNITY_Community 34|Community 34]]
|
||||
- [[_COMMUNITY_Community 35|Community 35]]
|
||||
- [[_COMMUNITY_Community 36|Community 36]]
|
||||
- [[_COMMUNITY_Community 37|Community 37]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `Period` - 78 edges
|
||||
2. `TotalLine` - 31 edges
|
||||
3. `compute_pnl()` - 17 edges
|
||||
4. `TestFusionReportEngine` - 14 edges
|
||||
5. `compute_balance_sheet()` - 13 edges
|
||||
6. `TestReportsController` - 12 edges
|
||||
7. `TestDatePeriods` - 12 edges
|
||||
8. `TestSeededReports` - 11 edges
|
||||
9. `compute_trial_balance()` - 11 edges
|
||||
10. `run()` - 11 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `Local LLM compat smoke for the commentary generator. Auto-detects an LM Studio` --uses--> `Period` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_local_llm_compat.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/date_periods.py
|
||||
- `Return (base_url, default_model) for the first reachable server, or (None, N` --uses--> `Period` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_local_llm_compat.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/date_periods.py
|
||||
- `Performance benchmarks with P95 targets, tagged 'benchmark'. These tests are no` --uses--> `Period` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_performance_benchmarks.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/date_periods.py
|
||||
- `Verify the seeded fusion.report definitions load and compute sensibly.` --uses--> `Period` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_seeded_reports.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/date_periods.py
|
||||
- `Unit tests for date_periods, account_hierarchy, totaling services.` --uses--> `Period` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_services_unit.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/date_periods.py
|
||||
|
||||
## Communities
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.05
|
||||
Nodes (46): Period, compute_balance_sheet(), compute_gl(), compute_pnl(), compute_trial_balance(), drill_down(), FusionReportEngine, The reports engine - orchestrator for all report computation. 5-method public A (+38 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.05
|
||||
Nodes (14): Coexistence tests for fusion_accounting_reports. Mirrors Phase 1's coexistence, The engine is registered regardless of Enterprise install state., TestReportsCoexistence, Tests for fusion.report.commentary cache model., TestFusionReportCommentary, Tests for fusion.report definition model., TestFusionReport, Tests for the 5 fusion AI tools registered in TOOL_DISPATCH. (+6 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.08
|
||||
Nodes (19): comparison_period(), fiscal_year_bounds(), month_bounds(), quarter_bounds(), Date period math for financial reports. Pure-Python helpers that compute: - Fis, Return the fiscal year period containing `reference_date`. Default: calenda, Return the calendar month containing `reference_date`., Return the calendar quarter containing `reference_date`. (+11 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.07
|
||||
Nodes (4): PeriodFilter, ReportTable, ReportViewer, ReportsService
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.1
|
||||
Nodes (12): Anomaly, detect(), Anomaly detection for financial reports. Compares each row's current-period amo, Detect anomalies in a report_result dict (engine output). Returns list of a, _cron_anomaly_scan(), _cron_mv_refresh(), FusionReportsCron, Cron handlers for fusion_accounting_reports. Two scheduled jobs: - _cron_anomal (+4 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.11
|
||||
Nodes (14): generate_commentary(), _get_provider(), AI-generated narrative commentary for financial reports. Takes a report_result, Generate narrative commentary via LLM. Returns dict per the contract. If no, No-LLM fallback that produces a basic narrative from the report data., Look up provider for 'reports_commentary' feature; return None if not configured, _templated_fallback(), Tests for commentary_generator service. (+6 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.14
|
||||
Nodes (7): HttpCase, _percentile(), Performance benchmarks with P95 targets, tagged 'benchmark'. These tests are no, TestControllerBenchmarks, TestEngineBenchmarks, Python wrappers that run the OWL tours via HttpCase.start_tour. Tours require a, TestReportsTours
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.18
|
||||
Nodes (10): AccountNode, build_tree(), filter_by_account_type(), Account hierarchy walker. Given a flat list of accounts with parent_id pointers, Build a forest from a flat list of account dicts. Each dict must have keys:, Depth-first walk yielding (node, depth, ancestors)., Return all nodes whose account_type starts with type_prefix (e.g. 'asset_' r, walk() (+2 more)
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.17
|
||||
Nodes (10): test_aggregate_sum_equals_input_sum(), test_balanced_iff_debits_equal_credits(), TestTotaling, aggregate(), aggregate_per_account(), is_balanced(), Move-line aggregation primitives for report totaling. Pure-Python helpers - cal, Aggregate a list of move-line dicts into a TotalLine. Each dict must have: (+2 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.22
|
||||
Nodes (11): compute_partner_grouped(), _build_period(), compare_periods(), drill_down(), export_xlsx(), FusionReportsController, get_anomalies(), get_commentary() (+3 more)
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.17
|
||||
Nodes (8): ConversionRate, convert_amount(), fetch_rates(), Multi-currency conversion for financial reports. Converts move-line amounts to, Convert `amount` from source to target at the given date. `rates` is a dict, Fetch all relevant rates from res.currency.rate as of a given date. Returns, Unit tests for currency_conversion service., TestCurrencyConversion
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.19
|
||||
Nodes (4): FusionReportAnomaly, Persisted anomaly flags from the engine's variance detection. Each row captures, Tests for fusion.report.anomaly model., TestFusionReportAnomaly
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.26
|
||||
Nodes (2): Controller tests using HttpCase for the 8 JSON-RPC endpoints., TestReportsController
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.23
|
||||
Nodes (6): DrillDownRow, fetch_drill_down(), Drill-down: from a report line to its underlying journal items. Given an accoun, Fetch journal items for an account within a date range. Returns flat list o, Tests for drill_down_resolver., TestDrillDownResolver
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.22
|
||||
Nodes (5): build_prompt(), LLM prompt for AI report commentary. Provider-agnostic system + user prompt bui, Build (system_prompt, user_prompt) tuple., Tests for commentary_prompt module., TestCommentaryPrompt
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.2
|
||||
Nodes (6): FusionMigrationWizard, Reports-specific migration step. Ensures the 4 CORE report definitions are pres, Verify all 4 CORE report definitions exist., Override to add reports-bootstrap step at the end of the chain., Tests for the reports-bootstrap migration step., TestMigrationRoundTrip
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.22
|
||||
Nodes (5): FusionAccountBalanceMV, Materialized view of per-account-per-month balances. Created lazily by init() (, _refresh(), Tests for fusion_account_balance MV., TestAccountBalanceMV
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.22
|
||||
Nodes (2): Verify the seeded fusion.report definitions load and compute sensibly., TestSeededReports
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.25
|
||||
Nodes (4): Tests for XLSX export wizard., TestXlsxExport, FusionXlsxExportWizard, XLSX export wizard for fusion financial reports.
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.5
|
||||
Nodes (1): DrillDownDialog
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.5
|
||||
Nodes (1): AnomalyStrip
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.67
|
||||
Nodes (1): Pre-migration: convert legacy act_window report actions to client actions. In 1
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.67
|
||||
Nodes (2): FusionReport, Persistent definition of a Fusion financial report. Each report (P&L, balance s
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.67
|
||||
Nodes (2): FusionReportCommentary, Cached AI-generated commentary for a report run. One row per (report, period_fr
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): AiCommentaryPanel
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 26 - "Community 26"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 27 - "Community 27"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 28 - "Community 28"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 31 - "Community 31"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 32 - "Community 32"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 33 - "Community 33"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Run last-month P&L vs prior-year-same-month and persist anomalies.
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): REFRESH CONCURRENTLY via dedicated autocommit cursor. REFRESH MATERIALI
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Refresh the MV. Falls back to non-concurrent if CONCURRENTLY fails. REF
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
## Knowledge Gaps
|
||||
- **66 isolated node(s):** `Pre-migration: convert legacy act_window report actions to client actions. In 1`, `Unit tests for anomaly_detection service.`, `Tests for commentary_prompt module.`, `Tests for the PDF export.`, `Tests for fusion.report.commentary cache model.` (+61 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **Thin community `Community 24`** (2 nodes): `AiCommentaryPanel`, `ai_commentary_panel.js`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 25`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 26`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 27`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 28`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 29`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 30`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 31`** (1 nodes): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 32`** (1 nodes): `__manifest__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 33`** (1 nodes): `Run last-month P&L vs prior-year-same-month and persist anomalies.`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 34`** (1 nodes): `REFRESH CONCURRENTLY via dedicated autocommit cursor. REFRESH MATERIALI`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 35`** (1 nodes): `Refresh the MV. Falls back to non-concurrent if CONCURRENTLY fails. REF`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 36`** (1 nodes): `reports_tours.js`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 37`** (1 nodes): `report_viewer_view.js`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `Period` connect `Community 0` to `Community 2`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 17`, `Community 18`?**
|
||||
_High betweenness centrality (0.301) - this node is a cross-community bridge._
|
||||
- **Why does `TestControllerBenchmarks` connect `Community 6` to `Community 0`?**
|
||||
_High betweenness centrality (0.085) - this node is a cross-community bridge._
|
||||
- **Why does `TestCurrencyConversion` connect `Community 10` to `Community 1`?**
|
||||
_High betweenness centrality (0.055) - this node is a cross-community bridge._
|
||||
- **Are the 72 inferred relationships involving `Period` (e.g. with `TestServiceInvariants` and `TestLineResolverInvariants`) actually correct?**
|
||||
_`Period` has 72 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 29 inferred relationships involving `TotalLine` (e.g. with `TestServiceInvariants` and `TestLineResolverInvariants`) actually correct?**
|
||||
_`TotalLine` has 29 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 14 inferred relationships involving `compute_pnl()` (e.g. with `.test_commentary_with_local_llm()` and `.test_compute_pnl_p95()`) actually correct?**
|
||||
_`compute_pnl()` has 14 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `Pre-migration: convert legacy act_window report actions to client actions. In 1`, `Unit tests for anomaly_detection service.`, `Tests for commentary_prompt module.` to the rest of the system?**
|
||||
_66 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/services/__init__.py", "source_location": "L9", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_drill_down_dialog_drill_down_dialog_js", "label": "drill_down_dialog.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L1"}, {"id": "drill_down_dialog_drilldowndialog", "label": "DrillDownDialog", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L5"}, {"id": "drill_down_dialog_drilldowndialog_formatamount", "label": ".formatAmount()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L12"}, {"id": "drill_down_dialog_drilldowndialog_onbackdropclick", "label": ".onBackdropClick()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L19"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_drill_down_dialog_drill_down_dialog_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_drill_down_dialog_drill_down_dialog_js", "target": "drill_down_dialog_drilldowndialog", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L5", "weight": 1.0}, {"source": "drill_down_dialog_drilldowndialog", "target": "drill_down_dialog_drilldowndialog_formatamount", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L12", "weight": 1.0}, {"source": "drill_down_dialog_drilldowndialog", "target": "drill_down_dialog_drilldowndialog_onbackdropclick", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "drill_down_dialog_drilldowndialog_formatamount", "callee": "format", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L14"}, {"caller_nid": "drill_down_dialog_drilldowndialog_onbackdropclick", "callee": "contains", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L20"}, {"caller_nid": "drill_down_dialog_drilldowndialog_onbackdropclick", "callee": "onClose", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/drill_down_dialog/drill_down_dialog.js", "source_location": "L21"}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_views_report_viewer_report_viewer_view_js", "label": "report_viewer_view.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/views/report_viewer/report_viewer_view.js", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_views_report_viewer_report_viewer_view_js", "target": "registry", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/views/report_viewer/report_viewer_view.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_views_report_viewer_report_viewer_view_js", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_views_report_viewer_report_viewer", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/views/report_viewer/report_viewer_view.js", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__init__.py", "source_location": "L5", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_tours_reports_tours_js", "label": "reports_tours.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/tours/reports_tours.js", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_tours_reports_tours_js", "target": "registry", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/tours/reports_tours.js", "source_location": "L3", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_py", "label": "fusion_report.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report.py", "source_location": "L1"}, {"id": "fusion_report_fusionreport", "label": "FusionReport", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report.py", "source_location": "L22"}, {"id": "fusion_report_rationale_1", "label": "Persistent definition of a Fusion financial report. Each report (P&L, balance s", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_py", "target": "fusion_report_fusionreport", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report.py", "source_location": "L22", "weight": 1.0}, {"source": "fusion_report_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_reports_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/reports/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/reports/__init__.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_migrations_19_0_1_1_2_pre_migration_py", "label": "pre-migration.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L1"}, {"id": "pre_migration_migrate", "label": "migrate()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L34"}, {"id": "pre_migration_rationale_1", "label": "Pre-migration: convert legacy act_window report actions to client actions. In 1", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_migrations_19_0_1_1_2_pre_migration_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L14", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_migrations_19_0_1_1_2_pre_migration_py", "target": "pre_migration_migrate", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L34", "weight": 1.0}, {"source": "pre_migration_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_migrations_19_0_1_1_2_pre_migration_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "pre_migration_migrate", "callee": "execute", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L39"}, {"caller_nid": "pre_migration_migrate", "callee": "fetchone", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L47"}, {"caller_nid": "pre_migration_migrate", "callee": "execute", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L53"}, {"caller_nid": "pre_migration_migrate", "callee": "execute", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L57"}, {"caller_nid": "pre_migration_migrate", "callee": "execute", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L61"}, {"caller_nid": "pre_migration_migrate", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L66"}, {"caller_nid": "pre_migration_migrate", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/migrations/19.0.1.1.2/pre-migration.py", "source_location": "L69"}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_anomaly_py", "label": "fusion_report_anomaly.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L1"}, {"id": "fusion_report_anomaly_fusionreportanomaly", "label": "FusionReportAnomaly", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L13"}, {"id": "fusion_report_anomaly_fusionreportanomaly_action_acknowledge", "label": ".action_acknowledge()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L45"}, {"id": "fusion_report_anomaly_fusionreportanomaly_action_dismiss", "label": ".action_dismiss()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L52"}, {"id": "fusion_report_anomaly_fusionreportanomaly_action_resolve", "label": ".action_resolve()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L55"}, {"id": "fusion_report_anomaly_rationale_1", "label": "Persisted anomaly flags from the engine's variance detection. Each row captures", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_anomaly_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_anomaly_py", "target": "fusion_report_anomaly_fusionreportanomaly", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L13", "weight": 1.0}, {"source": "fusion_report_anomaly_fusionreportanomaly", "target": "fusion_report_anomaly_fusionreportanomaly_action_acknowledge", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L45", "weight": 1.0}, {"source": "fusion_report_anomaly_fusionreportanomaly", "target": "fusion_report_anomaly_fusionreportanomaly_action_dismiss", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L52", "weight": 1.0}, {"source": "fusion_report_anomaly_fusionreportanomaly", "target": "fusion_report_anomaly_fusionreportanomaly_action_resolve", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L55", "weight": 1.0}, {"source": "fusion_report_anomaly_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_anomaly_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "fusion_report_anomaly_fusionreportanomaly_action_acknowledge", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L46"}, {"caller_nid": "fusion_report_anomaly_fusionreportanomaly_action_acknowledge", "callee": "now", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L49"}, {"caller_nid": "fusion_report_anomaly_fusionreportanomaly_action_dismiss", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L53"}, {"caller_nid": "fusion_report_anomaly_fusionreportanomaly_action_resolve", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_anomaly.py", "source_location": "L56"}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_account_balance_mv_py", "label": "test_account_balance_mv.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L1"}, {"id": "test_account_balance_mv_testaccountbalancemv", "label": "TestAccountBalanceMV", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L7"}, {"id": "transactioncase", "label": "TransactionCase", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "test_account_balance_mv_testaccountbalancemv_test_mv_exists_and_is_queryable", "label": ".test_mv_exists_and_is_queryable()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L9"}, {"id": "test_account_balance_mv_testaccountbalancemv_test_mv_refresh_concurrent", "label": ".test_mv_refresh_concurrent()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L15"}, {"id": "test_account_balance_mv_rationale_1", "label": "Tests for fusion_account_balance MV.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_account_balance_mv_py", "target": "odoo_tests_common", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_account_balance_mv_py", "target": "test_account_balance_mv_testaccountbalancemv", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L7", "weight": 1.0}, {"source": "test_account_balance_mv_testaccountbalancemv", "target": "transactioncase", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L7", "weight": 1.0}, {"source": "test_account_balance_mv_testaccountbalancemv", "target": "test_account_balance_mv_testaccountbalancemv_test_mv_exists_and_is_queryable", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L9", "weight": 1.0}, {"source": "test_account_balance_mv_testaccountbalancemv", "target": "test_account_balance_mv_testaccountbalancemv_test_mv_refresh_concurrent", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L15", "weight": 1.0}, {"source": "test_account_balance_mv_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_account_balance_mv_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "test_account_balance_mv_testaccountbalancemv_test_mv_exists_and_is_queryable", "callee": "_refresh", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L11"}, {"caller_nid": "test_account_balance_mv_testaccountbalancemv_test_mv_exists_and_is_queryable", "callee": "search", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L12"}, {"caller_nid": "test_account_balance_mv_testaccountbalancemv_test_mv_exists_and_is_queryable", "callee": "assertIsNotNone", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L13"}, {"caller_nid": "test_account_balance_mv_testaccountbalancemv_test_mv_refresh_concurrent", "callee": "_refresh", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L18"}, {"caller_nid": "test_account_balance_mv_testaccountbalancemv_test_mv_refresh_concurrent", "callee": "fail", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_account_balance_mv.py", "source_location": "L20"}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_commentary_py", "label": "fusion_report_commentary.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_commentary.py", "source_location": "L1"}, {"id": "fusion_report_commentary_fusionreportcommentary", "label": "FusionReportCommentary", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_commentary.py", "source_location": "L9"}, {"id": "fusion_report_commentary_rationale_1", "label": "Cached AI-generated commentary for a report run. One row per (report, period_fr", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_commentary.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_commentary_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_commentary.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_commentary_py", "target": "fusion_report_commentary_fusionreportcommentary", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_commentary.py", "source_location": "L9", "weight": 1.0}, {"source": "fusion_report_commentary_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_fusion_report_commentary_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/fusion_report_commentary.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_ai_commentary_panel_ai_commentary_panel_js", "label": "ai_commentary_panel.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.js", "source_location": "L1"}, {"id": "ai_commentary_panel_aicommentarypanel", "label": "AiCommentaryPanel", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.js", "source_location": "L5"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_ai_commentary_panel_ai_commentary_panel_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_ai_commentary_panel_ai_commentary_panel_js", "target": "ai_commentary_panel_aicommentarypanel", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/ai_commentary_panel/ai_commentary_panel.js", "source_location": "L5", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_wizards_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/wizards/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_wizards_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_wizards_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/wizards/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_wizards_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_wizards_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/wizards/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_cron_py", "label": "test_cron.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L1"}, {"id": "test_cron_testfusionreportscron", "label": "TestFusionReportsCron", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L7"}, {"id": "transactioncase", "label": "TransactionCase", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "test_cron_testfusionreportscron_setup", "label": ".setUp()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L9"}, {"id": "test_cron_testfusionreportscron_test_cron_mv_refresh_does_not_raise", "label": ".test_cron_mv_refresh_does_not_raise()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L13"}, {"id": "test_cron_testfusionreportscron_test_cron_anomaly_scan_does_not_raise", "label": ".test_cron_anomaly_scan_does_not_raise()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L18"}, {"id": "test_cron_rationale_1", "label": "Tests for cron handlers.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_cron_py", "target": "odoo_tests_common", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_cron_py", "target": "test_cron_testfusionreportscron", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L7", "weight": 1.0}, {"source": "test_cron_testfusionreportscron", "target": "transactioncase", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L7", "weight": 1.0}, {"source": "test_cron_testfusionreportscron", "target": "test_cron_testfusionreportscron_setup", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L9", "weight": 1.0}, {"source": "test_cron_testfusionreportscron", "target": "test_cron_testfusionreportscron_test_cron_mv_refresh_does_not_raise", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L13", "weight": 1.0}, {"source": "test_cron_testfusionreportscron", "target": "test_cron_testfusionreportscron_test_cron_anomaly_scan_does_not_raise", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L18", "weight": 1.0}, {"source": "test_cron_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_cron_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "test_cron_testfusionreportscron_setup", "callee": "super", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L10"}, {"caller_nid": "test_cron_testfusionreportscron_test_cron_mv_refresh_does_not_raise", "callee": "_cron_mv_refresh", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L16"}, {"caller_nid": "test_cron_testfusionreportscron_test_cron_anomaly_scan_does_not_raise", "callee": "_cron_anomaly_scan", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_cron.py", "source_location": "L20"}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_controllers_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/controllers/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_controllers_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_controllers_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/controllers/__init__.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_migration_round_trip_py", "label": "test_migration_round_trip.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L1"}, {"id": "test_migration_round_trip_testmigrationroundtrip", "label": "TestMigrationRoundTrip", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L7"}, {"id": "transactioncase", "label": "TransactionCase", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "label": ".test_bootstrap_finds_all_4_reports()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L9"}, {"id": "test_migration_round_trip_rationale_1", "label": "Tests for the reports-bootstrap migration step.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_migration_round_trip_py", "target": "odoo_tests_common", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_migration_round_trip_py", "target": "test_migration_round_trip_testmigrationroundtrip", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L7", "weight": 1.0}, {"source": "test_migration_round_trip_testmigrationroundtrip", "target": "transactioncase", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L7", "weight": 1.0}, {"source": "test_migration_round_trip_testmigrationroundtrip", "target": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L9", "weight": 1.0}, {"source": "test_migration_round_trip_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_tests_test_migration_round_trip_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "callee": "create", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L10"}, {"caller_nid": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "callee": "_reports_bootstrap_step", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L11"}, {"caller_nid": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "callee": "assertEqual", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L12"}, {"caller_nid": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "callee": "assertEqual", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L13"}, {"caller_nid": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "callee": "set", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L13"}, {"caller_nid": "test_migration_round_trip_testmigrationroundtrip_test_bootstrap_finds_all_4_reports", "callee": "assertEqual", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/tests/test_migration_round_trip.py", "source_location": "L15"}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_anomaly_strip_anomaly_strip_js", "label": "anomaly_strip.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L1"}, {"id": "anomaly_strip_anomalystrip", "label": "AnomalyStrip", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L11"}, {"id": "anomaly_strip_anomalystrip_alertclass", "label": ".alertClass()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L17"}, {"id": "anomaly_strip_anomalystrip_formatamount", "label": ".formatAmount()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L21"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_anomaly_strip_anomaly_strip_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_static_src_components_anomaly_strip_anomaly_strip_js", "target": "anomaly_strip_anomalystrip", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L11", "weight": 1.0}, {"source": "anomaly_strip_anomalystrip", "target": "anomaly_strip_anomalystrip_alertclass", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L17", "weight": 1.0}, {"source": "anomaly_strip_anomalystrip", "target": "anomaly_strip_anomalystrip_formatamount", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L21", "weight": 1.0}], "raw_calls": [{"caller_nid": "anomaly_strip_anomalystrip_formatamount", "callee": "format", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L23"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_reports_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_reports/models/__init__.py", "source_location": "L7", "weight": 1.0}], "raw_calls": []}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12871
fusion_accounting/fusion_accounting_reports/graphify-out/graph.json
Normal file
12871
fusion_accounting/fusion_accounting_reports/graphify-out/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
"""Pre-migration: convert legacy act_window report actions to client actions.
|
||||
|
||||
In 19.0.1.1.1 we shipped 11 ``ir.actions.act_window`` records with
|
||||
``view_mode='fusion_reports'``. Odoo's act_window resolver requires a
|
||||
matching ``ir.ui.view`` record per view_mode, so clicking those menus
|
||||
raised "View types not defined fusion_reports".
|
||||
|
||||
19.0.1.1.2 reissues the same xml_ids as ``ir.actions.client`` records
|
||||
with ``tag='fusion_reports'``. Odoo refuses to update a record across
|
||||
models, so this pre-migration drops the legacy records before the new
|
||||
data file loads. Idempotent: safe to re-run.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
LEGACY_XIDS = (
|
||||
'action_fusion_report_pnl',
|
||||
'action_fusion_report_balance_sheet',
|
||||
'action_fusion_report_trial_balance',
|
||||
'action_fusion_report_general_ledger',
|
||||
'action_fusion_report_cash_flow',
|
||||
'action_fusion_report_executive_summary',
|
||||
'action_fusion_report_annual_statements',
|
||||
'action_fusion_report_tax_summary',
|
||||
'action_fusion_report_aged_receivable',
|
||||
'action_fusion_report_aged_payable',
|
||||
'action_fusion_report_partner_ledger',
|
||||
)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if not version:
|
||||
return
|
||||
deleted = 0
|
||||
for name in LEGACY_XIDS:
|
||||
cr.execute(
|
||||
"""
|
||||
SELECT id, model, res_id
|
||||
FROM ir_model_data
|
||||
WHERE module = 'fusion_accounting_reports' AND name = %s
|
||||
""",
|
||||
(name,),
|
||||
)
|
||||
row = cr.fetchone()
|
||||
if not row:
|
||||
continue
|
||||
ir_md_id, model, res_id = row
|
||||
if model != 'ir.actions.act_window':
|
||||
continue
|
||||
cr.execute(
|
||||
"DELETE FROM ir_act_window WHERE id = %s",
|
||||
(res_id,),
|
||||
)
|
||||
cr.execute(
|
||||
"DELETE FROM ir_actions WHERE id = %s",
|
||||
(res_id,),
|
||||
)
|
||||
cr.execute(
|
||||
"DELETE FROM ir_model_data WHERE id = %s",
|
||||
(ir_md_id,),
|
||||
)
|
||||
deleted += 1
|
||||
_logger.info("Dropped legacy act_window for fusion_accounting_reports.%s", name)
|
||||
|
||||
if deleted:
|
||||
_logger.info(
|
||||
"fusion_accounting_reports pre-migration: dropped %d legacy "
|
||||
"act_window records to make way for ir.actions.client variants.",
|
||||
deleted,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from . import fusion_report
|
||||
from . import fusion_report_engine
|
||||
from . import fusion_report_commentary
|
||||
from . import fusion_report_anomaly
|
||||
from . import fusion_account_balance_mv
|
||||
from . import fusion_reports_cron
|
||||
from . import fusion_migration_wizard
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Materialized view of per-account-per-month balances.
|
||||
|
||||
Created lazily by init() (called by Odoo on install/upgrade). Refresh
|
||||
via the model's _refresh() method or via cron (Task 25)."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionAccountBalanceMV(models.Model):
|
||||
_name = "fusion.account.balance.mv"
|
||||
_description = "MV of per-account per-month aggregated balances"
|
||||
_auto = False
|
||||
_table = "fusion_account_balance_mv"
|
||||
_order = "period_month desc, account_id"
|
||||
|
||||
account_id = fields.Many2one('account.account', readonly=True)
|
||||
company_id = fields.Many2one('res.company', readonly=True)
|
||||
period_month = fields.Date(readonly=True)
|
||||
debit = fields.Float(readonly=True)
|
||||
credit = fields.Float(readonly=True)
|
||||
balance = fields.Float(readonly=True)
|
||||
line_count = fields.Integer(readonly=True)
|
||||
|
||||
def init(self):
|
||||
# If the MV exists but is missing the synthetic `id` column (e.g. from
|
||||
# an earlier dev install), drop it so the new schema applies cleanly.
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM pg_matviews mv
|
||||
JOIN pg_attribute a
|
||||
ON a.attrelid = (mv.schemaname || '.' || mv.matviewname)::regclass
|
||||
AND a.attname = 'id'
|
||||
WHERE mv.matviewname = 'fusion_account_balance_mv'
|
||||
"""
|
||||
)
|
||||
if not self.env.cr.fetchone():
|
||||
self.env.cr.execute(
|
||||
"DROP MATERIALIZED VIEW IF EXISTS fusion_account_balance_mv"
|
||||
)
|
||||
sql_path = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'data', 'sql',
|
||||
'create_mv_account_balance.sql',
|
||||
)
|
||||
with open(sql_path, 'r') as f:
|
||||
self.env.cr.execute(f.read())
|
||||
_logger.info(
|
||||
"fusion_account_balance_mv: created/verified MV + indexes")
|
||||
|
||||
@api.model
|
||||
def _refresh(self, *, concurrently=True):
|
||||
"""Refresh the MV. Falls back to non-concurrent if CONCURRENTLY fails.
|
||||
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY requires the MV to be already
|
||||
populated and an autocommit-capable cursor; the cron path in Task 25
|
||||
opens a dedicated cursor for that. This helper keeps callers safe by
|
||||
retrying without CONCURRENTLY on failure."""
|
||||
keyword = "CONCURRENTLY" if concurrently else ""
|
||||
try:
|
||||
self.env.cr.execute(
|
||||
f"REFRESH MATERIALIZED VIEW {keyword} fusion_account_balance_mv"
|
||||
)
|
||||
_logger.debug(
|
||||
"fusion_account_balance_mv refreshed (%s)",
|
||||
'concurrent' if concurrently else 'blocking',
|
||||
)
|
||||
except Exception as e:
|
||||
if concurrently:
|
||||
_logger.warning(
|
||||
"Concurrent MV refresh failed (%s); falling back", e)
|
||||
self.env.cr.execute(
|
||||
"REFRESH MATERIALIZED VIEW fusion_account_balance_mv"
|
||||
)
|
||||
else:
|
||||
raise
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Reports-specific migration step.
|
||||
|
||||
Ensures the 4 CORE report definitions are present after migration."""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionMigrationWizard(models.TransientModel):
|
||||
_inherit = "fusion.migration.wizard"
|
||||
|
||||
def _reports_bootstrap_step(self):
|
||||
"""Verify all 4 CORE report definitions exist."""
|
||||
Report = self.env['fusion.report'].sudo()
|
||||
expected = ['pnl', 'balance_sheet', 'trial_balance', 'general_ledger']
|
||||
present = Report.search([('report_type', 'in', expected)]).mapped('report_type')
|
||||
missing = set(expected) - set(present)
|
||||
return {
|
||||
'step': 'reports_bootstrap',
|
||||
'expected_reports': expected,
|
||||
'present_reports': list(present),
|
||||
'missing_reports': list(missing),
|
||||
}
|
||||
|
||||
def action_run_migration(self):
|
||||
"""Override to add reports-bootstrap step at the end of the chain."""
|
||||
result = super().action_run_migration() if hasattr(super(), 'action_run_migration') else None
|
||||
try:
|
||||
self._reports_bootstrap_step()
|
||||
except Exception as e:
|
||||
_logger.warning("reports_bootstrap_step failed: %s", e)
|
||||
return result
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Persistent definition of a Fusion financial report.
|
||||
|
||||
Each report (P&L, balance sheet, trial balance, GL) has ONE row in
|
||||
fusion.report describing its metadata + line specs. The line specs
|
||||
are stored as a JSON-typed field for flexibility (each line spec
|
||||
includes account_type filter, sub-totaling rules, sign convention)."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
REPORT_TYPES = [
|
||||
('pnl', 'Income Statement (P&L)'),
|
||||
('balance_sheet', 'Balance Sheet'),
|
||||
('trial_balance', 'Trial Balance'),
|
||||
('general_ledger', 'General Ledger'),
|
||||
('aged_receivable', 'Aged Receivable'),
|
||||
('aged_payable', 'Aged Payable'),
|
||||
('partner_ledger', 'Partner Ledger'),
|
||||
]
|
||||
|
||||
|
||||
class FusionReport(models.Model):
|
||||
_name = "fusion.report"
|
||||
_description = "Fusion Financial Report Definition"
|
||||
_order = "sequence, id"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(
|
||||
required=True,
|
||||
help="Unique technical code (e.g. 'pnl', 'balance_sheet').",
|
||||
)
|
||||
report_type = fields.Selection(REPORT_TYPES, required=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
description = fields.Text()
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
# Layout config - stored as JSON for flexibility per report type.
|
||||
# Example for P&L:
|
||||
# [
|
||||
# {"label": "Revenue", "account_type_prefix": "income_", "sign": 1},
|
||||
# {"label": "Cost of Goods Sold", "account_type_prefix": "expense_direct_", "sign": -1},
|
||||
# {"label": "Gross Profit", "compute": "subtotal", "above": 2},
|
||||
# ...
|
||||
# ]
|
||||
line_specs = fields.Json(string="Line Specs")
|
||||
|
||||
show_zero_balances = fields.Boolean(default=False)
|
||||
show_unposted = fields.Boolean(default=False)
|
||||
default_comparison_mode = fields.Selection(
|
||||
[
|
||||
('none', 'No comparison'),
|
||||
('previous_period', 'Previous Period'),
|
||||
('previous_year', 'Previous Year'),
|
||||
],
|
||||
default='none',
|
||||
)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
|
||||
_unique_company_code = models.Constraint(
|
||||
'UNIQUE(company_id, code)',
|
||||
'Report code must be unique per company.',
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Persisted anomaly flags from the engine's variance detection.
|
||||
|
||||
Each row captures one flagged report row variance. Used by the OWL
|
||||
anomaly_strip + the audit trail."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
SEVERITY = [('low', 'Low'), ('medium', 'Medium'), ('high', 'High')]
|
||||
DIRECTION = [('increase', 'Increase'), ('decrease', 'Decrease')]
|
||||
|
||||
|
||||
class FusionReportAnomaly(models.Model):
|
||||
_name = "fusion.report.anomaly"
|
||||
_description = "Flagged Report Variance"
|
||||
_order = "detected_at desc, severity desc"
|
||||
|
||||
report_id = fields.Many2one('fusion.report', required=True, ondelete='cascade')
|
||||
company_id = fields.Many2one('res.company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
period_from = fields.Date(required=True)
|
||||
period_to = fields.Date(required=True)
|
||||
|
||||
row_id = fields.Char(required=True, help="Engine-generated row id (e.g. 'line_3').")
|
||||
label = fields.Char(required=True)
|
||||
current_amount = fields.Float()
|
||||
comparison_amount = fields.Float()
|
||||
variance_amount = fields.Float()
|
||||
variance_pct = fields.Float()
|
||||
severity = fields.Selection(SEVERITY, required=True)
|
||||
direction = fields.Selection(DIRECTION, required=True)
|
||||
|
||||
detected_at = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||
state = fields.Selection([
|
||||
('new', 'New'),
|
||||
('acknowledged', 'Acknowledged'),
|
||||
('investigating', 'Investigating'),
|
||||
('resolved', 'Resolved'),
|
||||
('dismissed', 'Dismissed'),
|
||||
], default='new', required=True)
|
||||
notes = fields.Text()
|
||||
acknowledged_by = fields.Many2one('res.users')
|
||||
acknowledged_at = fields.Datetime()
|
||||
|
||||
def action_acknowledge(self):
|
||||
self.write({
|
||||
'state': 'acknowledged',
|
||||
'acknowledged_by': self.env.uid,
|
||||
'acknowledged_at': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
def action_dismiss(self):
|
||||
self.write({'state': 'dismissed'})
|
||||
|
||||
def action_resolve(self):
|
||||
self.write({'state': 'resolved'})
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Cached AI-generated commentary for a report run.
|
||||
|
||||
One row per (report, period_from, period_to, comparison_mode, company).
|
||||
Refreshed on demand or via cron when the underlying data has changed."""
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class FusionReportCommentary(models.Model):
|
||||
_name = "fusion.report.commentary"
|
||||
_description = "AI-Generated Report Commentary Cache"
|
||||
_order = "generated_at desc"
|
||||
|
||||
report_id = fields.Many2one('fusion.report', required=True, ondelete='cascade')
|
||||
company_id = fields.Many2one('res.company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
period_from = fields.Date(required=True)
|
||||
period_to = fields.Date(required=True)
|
||||
comparison_mode = fields.Selection([
|
||||
('none', 'None'),
|
||||
('previous_period', 'Previous Period'),
|
||||
('previous_year', 'Previous Year'),
|
||||
], default='none', required=True)
|
||||
|
||||
summary = fields.Text()
|
||||
highlights = fields.Json() # list of strings
|
||||
concerns = fields.Json() # list of strings
|
||||
next_actions = fields.Json() # list of strings
|
||||
|
||||
generated_at = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||
generated_by = fields.Selection([
|
||||
('on_demand', 'On Demand'),
|
||||
('cron', 'Cron'),
|
||||
('templated', 'Templated Fallback'),
|
||||
], default='on_demand', required=True)
|
||||
|
||||
provider = fields.Char(help="LLM provider used (e.g. 'openai', 'claude', 'local'). "
|
||||
"Empty for templated fallback.")
|
||||
|
||||
_unique_period = models.Constraint(
|
||||
'UNIQUE(report_id, company_id, period_from, period_to, comparison_mode)',
|
||||
'Only one commentary cache row per report+period+mode.',
|
||||
)
|
||||
@@ -0,0 +1,424 @@
|
||||
"""The reports engine - orchestrator for all report computation.
|
||||
|
||||
5-method public API. All controllers, AI tools, wizards, exports must
|
||||
go through these methods; no direct ORM aggregation queries from
|
||||
anywhere else.
|
||||
|
||||
Internal pipeline (per report run):
|
||||
1. Validate (period valid, company allowed, report exists)
|
||||
2. Fetch account hierarchy (cached per (company, fiscal_year))
|
||||
3. Aggregate move lines per account (the SQL workhorse)
|
||||
4. Resolve line_specs into report rows
|
||||
5. (Optional) Compute comparison-period rows
|
||||
6. (Optional) Detect anomalies (deferred to later tasks)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from ..services.account_hierarchy import build_tree
|
||||
from ..services.date_periods import Period, comparison_period as _comp_period
|
||||
from ..services.drill_down_resolver import fetch_drill_down
|
||||
from ..services.line_resolver import resolve as _resolve_lines
|
||||
from ..services.totaling import TotalLine
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionReportEngine(models.AbstractModel):
|
||||
_name = "fusion.report.engine"
|
||||
_description = "Fusion Financial Reports Engine"
|
||||
|
||||
# ============================================================
|
||||
# PUBLIC API (5 methods)
|
||||
# ============================================================
|
||||
|
||||
@api.model
|
||||
def compute_pnl(
|
||||
self, period: Period, *, comparison: str = 'none',
|
||||
company_id: int | None = None, report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""Income statement (P&L) for the given period.
|
||||
|
||||
``report_code`` selects between multiple PnL-typed report definitions
|
||||
(``pnl``, ``cash_flow``, ``executive_summary``, ``annual_statements``).
|
||||
When omitted, falls back to the canonical ``pnl`` definition.
|
||||
"""
|
||||
report = self._get_report(
|
||||
'pnl', company_id=company_id, code=report_code,
|
||||
)
|
||||
return self._compute(
|
||||
report, period, comparison=comparison, company_id=company_id,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def compute_balance_sheet(
|
||||
self, date_to: date, *, comparison: str = 'none',
|
||||
company_id: int | None = None, report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""Balance sheet AS OF date_to. Period.date_from is set to a
|
||||
far-past date so balances are cumulative-since-inception."""
|
||||
report = self._get_report(
|
||||
'balance_sheet', company_id=company_id, code=report_code,
|
||||
)
|
||||
period = Period(
|
||||
date_from=date(1970, 1, 1),
|
||||
date_to=date_to,
|
||||
label=f"As of {date_to}",
|
||||
)
|
||||
return self._compute(
|
||||
report, period, comparison=comparison, company_id=company_id,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def compute_trial_balance(
|
||||
self, period: Period, *, company_id: int | None = None,
|
||||
report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""Trial balance for the given period - every account with
|
||||
non-zero balance.
|
||||
|
||||
``report_code`` selects between multiple TB-typed reports (e.g.
|
||||
``trial_balance``, ``tax_summary``).
|
||||
"""
|
||||
report = self._get_report(
|
||||
'trial_balance', company_id=company_id, code=report_code,
|
||||
)
|
||||
return self._compute(
|
||||
report, period, comparison='none', company_id=company_id,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def compute_gl(
|
||||
self, period: Period, *, account_ids: list | None = None,
|
||||
company_id: int | None = None, report_code: str | None = None,
|
||||
) -> dict:
|
||||
"""General ledger for the given period.
|
||||
|
||||
Returns per-account move-line listings rather than aggregated rows."""
|
||||
report = self._get_report(
|
||||
'general_ledger', company_id=company_id, code=report_code,
|
||||
)
|
||||
company_id = company_id or self.env.company.id
|
||||
result = self._compute(
|
||||
report, period, comparison='none', company_id=company_id,
|
||||
)
|
||||
gl_by_account = {}
|
||||
target_ids = account_ids or list(result.get('account_totals', {}).keys())
|
||||
for acct_id in target_ids:
|
||||
gl_by_account[acct_id] = fetch_drill_down(
|
||||
self.env,
|
||||
account_id=acct_id,
|
||||
date_from=period.date_from,
|
||||
date_to=period.date_to,
|
||||
company_id=company_id,
|
||||
limit=200,
|
||||
)
|
||||
result['gl_by_account'] = gl_by_account
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def drill_down(
|
||||
self, *, account_id: int, period: Period,
|
||||
company_id: int | None = None,
|
||||
) -> list:
|
||||
"""Drill into a report line: list the journal items behind it."""
|
||||
company_id = company_id or self.env.company.id
|
||||
return fetch_drill_down(
|
||||
self.env,
|
||||
account_id=account_id,
|
||||
date_from=period.date_from,
|
||||
date_to=period.date_to,
|
||||
company_id=company_id,
|
||||
limit=500,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def compute_partner_grouped(
|
||||
self, period: Period, *, account_type: str = 'asset_receivable',
|
||||
comparison: str = 'none', company_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Per-partner aggregation report (Aged Receivable, Aged Payable,
|
||||
Partner Ledger).
|
||||
|
||||
Returns a dict with ``rows`` = list of partner-level aggregates.
|
||||
Each row has the partner_id, partner_name, total residual, and
|
||||
aging buckets: current / 1-30 / 31-60 / 61-90 / 90+ days past
|
||||
``period.date_to``.
|
||||
|
||||
SQL-direct for performance: a single GROUP BY query with conditional
|
||||
sum per bucket. Only un-reconciled, posted lines with non-zero
|
||||
residual at the as-of date are included.
|
||||
"""
|
||||
company_id = company_id or self.env.company.id
|
||||
|
||||
accounts = self.env['account.account'].sudo().search([
|
||||
('account_type', '=', account_type),
|
||||
('company_ids', 'in', company_id),
|
||||
])
|
||||
if not accounts:
|
||||
return {
|
||||
'report_type': 'partner_grouped',
|
||||
'account_type': account_type,
|
||||
'period': {
|
||||
'date_from': str(period.date_from),
|
||||
'date_to': str(period.date_to),
|
||||
'label': period.label,
|
||||
},
|
||||
'rows': [],
|
||||
'total': 0.0,
|
||||
'partner_count': 0,
|
||||
}
|
||||
|
||||
as_of = period.date_to
|
||||
d30 = as_of - timedelta(days=30)
|
||||
d60 = as_of - timedelta(days=60)
|
||||
d90 = as_of - timedelta(days=90)
|
||||
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT
|
||||
COALESCE(p.id, 0) AS partner_id,
|
||||
COALESCE(p.name, '(no partner)') AS partner_name,
|
||||
SUM(aml.amount_residual) AS total_residual,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity >= %s
|
||||
OR aml.date_maturity IS NULL
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_current,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
AND aml.date_maturity >= %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_1_30,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
AND aml.date_maturity >= %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_31_60,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
AND aml.date_maturity >= %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_61_90,
|
||||
SUM(CASE
|
||||
WHEN aml.date_maturity < %s
|
||||
THEN aml.amount_residual ELSE 0
|
||||
END) AS bucket_90_plus,
|
||||
COUNT(*) AS line_count
|
||||
FROM account_move_line aml
|
||||
LEFT JOIN res_partner p ON p.id = aml.partner_id
|
||||
WHERE aml.account_id = ANY(%s)
|
||||
AND aml.parent_state = 'posted'
|
||||
AND aml.reconciled = false
|
||||
AND aml.amount_residual != 0
|
||||
AND aml.company_id = %s
|
||||
AND aml.date <= %s
|
||||
GROUP BY p.id, p.name
|
||||
HAVING SUM(aml.amount_residual) != 0
|
||||
ORDER BY total_residual DESC
|
||||
""",
|
||||
(
|
||||
as_of,
|
||||
as_of, d30,
|
||||
d30, d60,
|
||||
d60, d90,
|
||||
d90,
|
||||
list(accounts.ids), company_id, as_of,
|
||||
),
|
||||
)
|
||||
|
||||
rows = []
|
||||
for r in self.env.cr.dictfetchall():
|
||||
rows.append({
|
||||
'partner_id': r['partner_id'] or False,
|
||||
'partner_name': r['partner_name'] or '(no partner)',
|
||||
'total': float(r['total_residual'] or 0),
|
||||
'bucket_current': float(r['bucket_current'] or 0),
|
||||
'bucket_1_30': float(r['bucket_1_30'] or 0),
|
||||
'bucket_31_60': float(r['bucket_31_60'] or 0),
|
||||
'bucket_61_90': float(r['bucket_61_90'] or 0),
|
||||
'bucket_90_plus': float(r['bucket_90_plus'] or 0),
|
||||
'line_count': r['line_count'],
|
||||
})
|
||||
|
||||
total = sum(r['total'] for r in rows)
|
||||
return {
|
||||
'report_type': 'partner_grouped',
|
||||
'account_type': account_type,
|
||||
'period': {
|
||||
'date_from': str(period.date_from),
|
||||
'date_to': str(period.date_to),
|
||||
'label': period.label,
|
||||
},
|
||||
'company_id': company_id,
|
||||
'rows': rows,
|
||||
'total': total,
|
||||
'partner_count': len(rows),
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# PRIVATE HELPERS
|
||||
# ============================================================
|
||||
|
||||
def _get_report(
|
||||
self, report_type: str, *, company_id: int | None = None,
|
||||
code: str | None = None,
|
||||
):
|
||||
"""Look up the active fusion.report definition.
|
||||
|
||||
When ``code`` is provided, prefer the report with that exact code
|
||||
(validating its ``report_type`` matches). Otherwise fall back to
|
||||
the canonical-by-type lookup: prefer code == report_type, then any
|
||||
report of that type. Per-company overrides win over global.
|
||||
"""
|
||||
Report = self.env['fusion.report'].sudo()
|
||||
company_id = company_id or self.env.company.id
|
||||
company_domain = [
|
||||
('active', '=', True),
|
||||
'|',
|
||||
('company_id', '=', company_id),
|
||||
('company_id', '=', False),
|
||||
]
|
||||
if code:
|
||||
report = Report.search(
|
||||
[('code', '=', code)] + company_domain,
|
||||
order='company_id desc nulls last',
|
||||
limit=1,
|
||||
)
|
||||
if not report:
|
||||
raise ValidationError(
|
||||
_("No active fusion.report definition with code '%s'") % code
|
||||
)
|
||||
if report.report_type != report_type:
|
||||
raise ValidationError(
|
||||
_("Report '%(code)s' has type '%(actual)s' but '%(expected)s' was expected.")
|
||||
% {
|
||||
'code': code,
|
||||
'actual': report.report_type,
|
||||
'expected': report_type,
|
||||
}
|
||||
)
|
||||
return report
|
||||
|
||||
# No code: prefer the canonical (code == report_type), then any
|
||||
# other report of that type.
|
||||
report = Report.search(
|
||||
[('code', '=', report_type), ('report_type', '=', report_type)] + company_domain,
|
||||
order='company_id desc nulls last',
|
||||
limit=1,
|
||||
)
|
||||
if report:
|
||||
return report
|
||||
report = Report.search(
|
||||
[('report_type', '=', report_type)] + company_domain,
|
||||
order='company_id desc nulls last, sequence',
|
||||
limit=1,
|
||||
)
|
||||
if not report:
|
||||
raise ValidationError(
|
||||
_("No active fusion.report definition for type '%s'") % report_type
|
||||
)
|
||||
return report
|
||||
|
||||
def _fetch_accounts(self, company_id):
|
||||
"""Fetch all accounts for a company, return flat dict + tree."""
|
||||
Account = self.env['account.account'].sudo()
|
||||
records = Account.search([('company_ids', 'in', company_id)])
|
||||
# account.account doesn't carry a parent_id in V19 - we use
|
||||
# account_type prefixes instead, so parent_id is always None here.
|
||||
flat = [
|
||||
{
|
||||
'id': a.id,
|
||||
'code': a.code,
|
||||
'name': a.name,
|
||||
'account_type': a.account_type or '',
|
||||
'parent_id': None,
|
||||
}
|
||||
for a in records
|
||||
]
|
||||
accounts_by_id = {a['id']: a for a in flat}
|
||||
tree = build_tree(flat)
|
||||
return accounts_by_id, tree
|
||||
|
||||
def _aggregate_period(self, period: Period, company_id: int) -> dict:
|
||||
"""SQL aggregate per account_id for a period.
|
||||
|
||||
Raw SQL for performance; this is the perf-critical step."""
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT account_id,
|
||||
COALESCE(SUM(debit), 0) AS d,
|
||||
COALESCE(SUM(credit), 0) AS c,
|
||||
COALESCE(SUM(balance), 0) AS b
|
||||
FROM account_move_line
|
||||
WHERE parent_state = 'posted'
|
||||
AND company_id = %s
|
||||
AND date >= %s
|
||||
AND date <= %s
|
||||
GROUP BY account_id
|
||||
""",
|
||||
(company_id, period.date_from, period.date_to),
|
||||
)
|
||||
out = {}
|
||||
for row in self.env.cr.fetchall():
|
||||
out[row[0]] = TotalLine(
|
||||
debit=float(row[1] or 0),
|
||||
credit=float(row[2] or 0),
|
||||
balance=float(row[3] or 0),
|
||||
)
|
||||
return out
|
||||
|
||||
def _compute(
|
||||
self, report, period: Period, *, comparison: str,
|
||||
company_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Shared computation pipeline. Returns dict with rows, totals,
|
||||
metadata."""
|
||||
company_id = company_id or self.env.company.id
|
||||
|
||||
accounts_by_id, _tree = self._fetch_accounts(company_id)
|
||||
|
||||
account_totals = self._aggregate_period(period, company_id)
|
||||
|
||||
comp_totals = None
|
||||
comp_period = None
|
||||
if comparison and comparison != 'none':
|
||||
comp_period = _comp_period(period, comparison)
|
||||
if comp_period:
|
||||
comp_totals = self._aggregate_period(comp_period, company_id)
|
||||
|
||||
rows = _resolve_lines(
|
||||
report.line_specs or [],
|
||||
account_totals=account_totals,
|
||||
accounts_by_id=accounts_by_id,
|
||||
comparison_totals=comp_totals,
|
||||
)
|
||||
|
||||
return {
|
||||
'report_id': report.id,
|
||||
'report_name': report.name,
|
||||
'report_type': report.report_type,
|
||||
'period': {
|
||||
'date_from': str(period.date_from),
|
||||
'date_to': str(period.date_to),
|
||||
'label': period.label,
|
||||
},
|
||||
'comparison_period': (
|
||||
{
|
||||
'date_from': str(comp_period.date_from),
|
||||
'date_to': str(comp_period.date_to),
|
||||
'label': comp_period.label,
|
||||
}
|
||||
if comp_period
|
||||
else None
|
||||
),
|
||||
'company_id': company_id,
|
||||
'rows': rows,
|
||||
'account_totals': {
|
||||
aid: tl.balance for aid, tl in account_totals.items()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Cron handlers for fusion_accounting_reports.
|
||||
|
||||
Two scheduled jobs:
|
||||
- _cron_anomaly_scan: daily P&L variance scan -> persist anomalies
|
||||
- _cron_mv_refresh: every 15 min CONCURRENTLY refresh the MV"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import odoo
|
||||
from odoo import api, fields, models
|
||||
|
||||
from ..services.anomaly_detection import detect
|
||||
from ..services.date_periods import month_bounds
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionReportsCron(models.AbstractModel):
|
||||
_name = "fusion.reports.cron"
|
||||
_description = "Fusion Reports Cron Handlers"
|
||||
|
||||
@api.model
|
||||
def _cron_anomaly_scan(self):
|
||||
"""Run last-month P&L vs prior-year-same-month and persist anomalies."""
|
||||
today = fields.Date.today()
|
||||
# Walk back into the previous full calendar month.
|
||||
last_month = today.replace(day=1) - timedelta(days=1)
|
||||
period = month_bounds(last_month)
|
||||
|
||||
Report = self.env['fusion.report'].sudo()
|
||||
Anomaly = self.env['fusion.report.anomaly'].sudo()
|
||||
engine = self.env['fusion.report.engine']
|
||||
|
||||
for company in self.env['res.company'].search([]):
|
||||
try:
|
||||
pnl_def = Report.search(
|
||||
[
|
||||
('report_type', '=', 'pnl'),
|
||||
'|', ('company_id', '=', company.id),
|
||||
('company_id', '=', False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if not pnl_def:
|
||||
continue
|
||||
result = engine.compute_pnl(
|
||||
period,
|
||||
comparison='previous_year',
|
||||
company_id=company.id,
|
||||
)
|
||||
anomalies = detect(result)
|
||||
for a in anomalies:
|
||||
existing = Anomaly.search(
|
||||
[
|
||||
('report_id', '=', pnl_def.id),
|
||||
('company_id', '=', company.id),
|
||||
('period_from', '=', period.date_from),
|
||||
('period_to', '=', period.date_to),
|
||||
('row_id', '=', a['row_id']),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
vals = {
|
||||
'report_id': pnl_def.id,
|
||||
'company_id': company.id,
|
||||
'period_from': period.date_from,
|
||||
'period_to': period.date_to,
|
||||
'row_id': a['row_id'],
|
||||
'label': a['label'],
|
||||
'current_amount': a['current_amount'],
|
||||
'comparison_amount': a['comparison_amount'],
|
||||
'variance_amount': a['variance_amount'],
|
||||
'variance_pct': a['variance_pct'],
|
||||
'severity': a['severity'],
|
||||
'direction': a['direction'],
|
||||
}
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
else:
|
||||
Anomaly.create(vals)
|
||||
_logger.info(
|
||||
"Anomaly scan for company %s: %d flagged",
|
||||
company.id, len(anomalies),
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.exception(
|
||||
"Anomaly scan failed for company %s: %s", company.id, e,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _cron_mv_refresh(self):
|
||||
"""REFRESH CONCURRENTLY via dedicated autocommit cursor.
|
||||
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY cannot run inside a
|
||||
transaction block, so we open a separate connection with autocommit
|
||||
enabled. The blocking REFRESH is used as a fallback if the
|
||||
concurrent path fails (e.g. on a cold MV with no rows yet)."""
|
||||
try:
|
||||
db_name = self.env.cr.dbname
|
||||
db = odoo.sql_db.db_connect(db_name)
|
||||
with db.cursor() as cron_cr:
|
||||
cron_cr._cnx.set_session(autocommit=True)
|
||||
cron_cr.execute(
|
||||
"REFRESH MATERIALIZED VIEW CONCURRENTLY "
|
||||
"fusion_account_balance_mv"
|
||||
)
|
||||
_logger.debug("MV refresh CONCURRENTLY succeeded")
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
"CONCURRENTLY refresh failed (%s); blocking fallback", e)
|
||||
try:
|
||||
self.env['fusion.account.balance.mv']._refresh(
|
||||
concurrently=False)
|
||||
except Exception as e2:
|
||||
_logger.exception(
|
||||
"Blocking MV refresh also failed: %s", e2)
|
||||
@@ -0,0 +1 @@
|
||||
from . import report_pdf
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user