changes
This commit is contained in:
BIN
fusion_accounting/fusion_accounting_assets/.DS_Store
vendored
Normal file
BIN
fusion_accounting/fusion_accounting_assets/.DS_Store
vendored
Normal file
Binary file not shown.
130
fusion_accounting/fusion_accounting_assets/CLAUDE.md
Normal file
130
fusion_accounting/fusion_accounting_assets/CLAUDE.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# fusion_accounting_assets — Cursor / Claude Context
|
||||
|
||||
## Purpose
|
||||
|
||||
AI-augmented fixed asset management with depreciation schedules — a
|
||||
Fusion-native replacement for (and coexisting with) Odoo Enterprise's
|
||||
`account_asset` module. Ships in Phase 3 of the fusion_accounting roadmap.
|
||||
|
||||
## Architecture
|
||||
|
||||
Hybrid: the engine (`fusion.asset.engine`, AbstractModel) is the SINGLE
|
||||
write surface for the asset lifecycle. Everything else (controllers, OWL
|
||||
widget, AI tools, wizards, cron) routes through the engine's 7-method
|
||||
public API:
|
||||
|
||||
- `compute_depreciation_schedule(asset, recompute=False)`
|
||||
- `post_depreciation_entry(asset, period_date=None)`
|
||||
- `dispose_asset(asset, sale_amount=0, sale_date=None, sale_partner=None, disposal_type='sale')`
|
||||
- `partial_sale(asset, sold_amount, sold_qty=None, sale_date=None, sale_partner=None)`
|
||||
- `pause_asset(asset, pause_date=None)`
|
||||
- `resume_asset(asset, resume_date=None)`
|
||||
- `reverse_disposal(asset)`
|
||||
|
||||
Pure-Python services live in `services/`:
|
||||
|
||||
- `depreciation_methods` — straight_line, declining_balance, units_of_production
|
||||
- `prorate` — first/last-period prorating: full_month, days_365, days_period
|
||||
- `salvage_value` — % of cost, fixed amount, zero
|
||||
- `anomaly_detection` — variance vs expected schedule, low utilization
|
||||
- `useful_life_predictor` + `useful_life_prompt` — LLM-suggested useful life with templated fallback
|
||||
|
||||
Persisted models in `models/`:
|
||||
|
||||
- `fusion.asset` — main model, state machine: draft → running → paused → disposed
|
||||
- `fusion.asset.depreciation.line` — board lines
|
||||
- `fusion.asset.category` — templates
|
||||
- `fusion.asset.disposal` — disposal records
|
||||
- `fusion.asset.anomaly` — flagged variances
|
||||
- `fusion.asset.book.values.mv` — pre-aggregated materialized view
|
||||
- `fusion.asset.engine` — AbstractModel (the API)
|
||||
- `fusion.assets.cron` — cron handlers (post depreciations, MV refresh, anomaly scan)
|
||||
- `account.move.line` (inherits) — adds `fusion_asset_id` linkage
|
||||
- `fusion.migration.wizard` (inherits in `models/`) — adds asset backfill step
|
||||
|
||||
Wizards (TransientModel) in `wizards/`:
|
||||
|
||||
- `fusion.create.asset.wizard` — assisted creation with AI useful-life suggestion
|
||||
- `fusion.disposal.wizard` — full disposal flow
|
||||
- `fusion.partial.sale.wizard` — partial-quantity disposal
|
||||
- `fusion.depreciation.run.wizard` — period close runner
|
||||
|
||||
Controller: `controllers/assets_controller.py` exposes 8 JSON-RPC
|
||||
endpoints under `/fusion/assets/*` (list, get_detail, compute_schedule,
|
||||
post_depreciation, dispose, get_anomalies, suggest_useful_life,
|
||||
get_partner_history). All calls route through the engine.
|
||||
|
||||
OWL frontend: `static/src/`
|
||||
|
||||
- `services/assets_service.js` — central reactive state + RPC wrappers
|
||||
- `views/asset_dashboard/*` — top-level dashboard view
|
||||
- `components/asset_card`, `asset_detail_panel`, `depreciation_board`,
|
||||
`disposal_dialog`, `ai_useful_life_panel`, `anomaly_strip` — 6 components
|
||||
- `scss/_variables.scss` + `assets.scss` + `dark_mode.scss`
|
||||
- `tours/assets_tours.js` — 5 OWL tour smoke tests
|
||||
|
||||
## Coexistence
|
||||
|
||||
When `account_asset` is installed the Asset Management menu hides via
|
||||
`fusion_accounting_core.group_fusion_show_when_enterprise_absent` (a
|
||||
computed group). The engine + AI tools remain available for the chat.
|
||||
The migration wizard backfills `fusion.asset` from existing
|
||||
`account.asset` records (verified live: 2 records, Task 35).
|
||||
|
||||
## 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`).
|
||||
- **Materialized view refresh:** `fusion.asset.book.values.mv` is
|
||||
refreshed by cron (REFRESH CONCURRENTLY in an autocommit cursor since
|
||||
it can't run inside a regular Odoo transaction).
|
||||
- **Provider routing:** AI features look up
|
||||
`fusion_accounting.provider.asset_useful_life`, falling back to
|
||||
`fusion_accounting.provider.default`. When neither is set the
|
||||
templated keyword fallback in `useful_life_predictor` keeps the
|
||||
feature usable offline.
|
||||
|
||||
## Performance baseline (Tasks 23 + 41)
|
||||
|
||||
| Operation | P95 | Budget | Headroom |
|
||||
|------------------------------------|-------|----------|----------|
|
||||
| `engine.compute_schedule` (10yr SL)| 1ms | 500ms | 500x |
|
||||
| `engine.post_depreciation_entry` | <1ms | 300ms | huge |
|
||||
| `engine.dispose_asset` | 5ms | 300ms | 60x |
|
||||
| `controller.list` (35 assets) | 42ms | 300ms | 7x |
|
||||
| `controller.get_detail` | 40ms | 500ms | 12x |
|
||||
|
||||
All Phase 3 perf metrics are within 1x of budget; no optimization was
|
||||
needed at ship (Task 42 skipped per the conditional rule).
|
||||
|
||||
## Test counts (Phase 3 ship)
|
||||
|
||||
- 140 logical tests total in fusion_accounting_assets
|
||||
- 0 failures, 0 errors
|
||||
- Coverage includes: 4 engine benchmarks + 1 controller benchmark
|
||||
(tagged `benchmark`), 1 local LLM smoke (tagged `local_llm`, skips
|
||||
when no LLM), 5 OWL tour tests (tagged `tour`, skip without
|
||||
websocket-client), Hypothesis property tests on the engine,
|
||||
integration tests on the public API, controller round-trip tests, MV
|
||||
shape tests.
|
||||
|
||||
## Known concerns / Phase 3.5 backlog
|
||||
|
||||
- Sub-annual depreciation frequency (currently annual only)
|
||||
- Units-of-production assumes even per-period units
|
||||
- Disposal journal entry not yet created — `dispose_asset` writes the
|
||||
`fusion.asset.disposal` record but not the cash / gain-loss move
|
||||
- Multi-currency, allocation rules, and analytic tags for depreciation
|
||||
moves are out of scope for Phase 3
|
||||
- Partial-sale child asset is created with no own depreciation schedule
|
||||
pre-disposal
|
||||
- Migration wizard inheritance lives in `models/` rather than
|
||||
`wizards/` (small inconsistency with the rest of the wizard layout —
|
||||
intentional to keep ORM ordering simple)
|
||||
- `useful_life_predictor` always returns a usable dict (templated
|
||||
fallback when LLM absent), so callers can't distinguish "AI said so"
|
||||
from "fallback fired"; the `confidence` key is the only signal
|
||||
53
fusion_accounting/fusion_accounting_assets/README.md
Normal file
53
fusion_accounting/fusion_accounting_assets/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# fusion_accounting_assets
|
||||
|
||||
AI-augmented fixed asset management for Odoo 19 Community — a
|
||||
Fusion-native replacement for Enterprise's `account_asset` module.
|
||||
|
||||
## What it does
|
||||
|
||||
- Three depreciation methods: straight-line, declining balance, and
|
||||
units-of-production
|
||||
- Asset lifecycle state machine: draft → running → paused → disposed
|
||||
- Editable depreciation board with full schedule recompute
|
||||
- Disposal flow (sale, scrap, donation) plus partial-sale wizard
|
||||
- Daily cron for posting periodic depreciation
|
||||
- AI augmentation:
|
||||
- **Anomaly detection** — variance vs expected schedule, low utilization
|
||||
- **Useful-life suggestion** — LLM-driven from invoice context, with a
|
||||
keyword-based templated fallback so the feature still works offline
|
||||
- Coexists with Enterprise `account_asset` (Enterprise wins by default;
|
||||
the Fusion menu only appears when Enterprise is uninstalled)
|
||||
- Migration-aware: bootstrap step backfills `fusion.asset` from existing
|
||||
`account.asset` rows so the AI has memory from day 1
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Install
|
||||
odoo --addons-path=... -i fusion_accounting_assets
|
||||
|
||||
# Open the dashboard (when Enterprise's account_asset is NOT installed)
|
||||
# Apps -> Asset Management -> Assets
|
||||
|
||||
# When Enterprise IS installed: use Enterprise's UI; the engine + AI tools
|
||||
# are still available via the AI chat.
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
- Local LLM (LM Studio, Ollama):
|
||||
- `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` (anything non-empty)
|
||||
- `fusion_accounting.provider.asset_useful_life` = `openai`
|
||||
|
||||
## Public API (engine)
|
||||
|
||||
`fusion.asset.engine` is the single write surface. See `CLAUDE.md` for
|
||||
the full 7-method signature list.
|
||||
|
||||
## See also
|
||||
|
||||
- `CLAUDE.md` — agent context
|
||||
- `UPGRADE_NOTES.md` — Odoo version anchoring
|
||||
49
fusion_accounting/fusion_accounting_assets/UPGRADE_NOTES.md
Normal file
49
fusion_accounting/fusion_accounting_assets/UPGRADE_NOTES.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# fusion_accounting_assets — Upgrade Notes
|
||||
|
||||
## Odoo Version Anchor
|
||||
|
||||
This module targets **Odoo 19.0** (community-base).
|
||||
|
||||
Reference snapshot of Enterprise code mirrored from:
|
||||
- `account_asset` (Odoo 19.0.x)
|
||||
- Source: `/Users/gurpreet/Github/RePackaged-Odoo/accounting/account_asset/`
|
||||
|
||||
## 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.asset` / `account.move.line` API
|
||||
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 against the new Odoo version
|
||||
5. Update this file with the new version anchor + any deviations
|
||||
|
||||
## V19 Migration Notes (already applied)
|
||||
|
||||
- `_sql_constraints` → `models.Constraint` (every persisted model)
|
||||
- `@api.depends('id')` → removed (none introduced)
|
||||
- `@route(type='json')` → `type='jsonrpc'` (all 8 endpoints in
|
||||
`controllers/assets_controller.py`)
|
||||
- `numbercall` removed from `ir.cron` (data/cron.xml)
|
||||
- `res.groups.users` → `user_ids` and `ir.ui.menu.groups_id` →
|
||||
`group_ids` (security + menu_views.xml)
|
||||
|
||||
## Phase 3 → Phase 3.5 Migration
|
||||
|
||||
If we ship Phase 3.5 (sub-annual depreciation frequency, disposal journal
|
||||
entries, multi-currency, allocation rules), changes will go in
|
||||
incremental commits. No DB migration needed (Phase 3 schema is
|
||||
forward-compatible — new columns will be nullable / default-valued).
|
||||
|
||||
## Coexistence with Enterprise `account_asset`
|
||||
|
||||
The migration step in `fusion.migration.wizard` backfills `fusion.asset`
|
||||
records from existing `account.asset` rows. It is idempotent (skips rows
|
||||
already linked via the `legacy_account_asset_id` column). Verified live
|
||||
on westin-v19: 2 records migrated cleanly.
|
||||
|
||||
When `account_asset` is installed the Asset Management menu hides via
|
||||
`fusion_accounting_core.group_fusion_show_when_enterprise_absent`. The
|
||||
engine and AI tools remain available for chat-driven workflows.
|
||||
5
fusion_accounting/fusion_accounting_assets/__init__.py
Normal file
5
fusion_accounting/fusion_accounting_assets/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from . import models
|
||||
from . import services
|
||||
from . import controllers
|
||||
from . import wizards
|
||||
from . import reports
|
||||
75
fusion_accounting/fusion_accounting_assets/__manifest__.py
Normal file
75
fusion_accounting/fusion_accounting_assets/__manifest__.py
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
'name': 'Fusion Accounting Assets',
|
||||
'version': '19.0.1.1.1',
|
||||
'category': 'Accounting/Accounting',
|
||||
'summary': 'AI-augmented asset management with depreciation schedules.',
|
||||
'description': """
|
||||
Fusion Accounting Assets
|
||||
========================
|
||||
|
||||
A Fusion-native replacement for Odoo Enterprise's account_asset module.
|
||||
|
||||
CORE scope (Phase 3):
|
||||
- 3 depreciation methods: straight-line, declining balance, units of production
|
||||
- Asset lifecycle: draft -> running -> paused -> disposed
|
||||
- Depreciation board with editable schedule
|
||||
- Disposal (sale, scrap, donation) + partial sale wizards
|
||||
- Daily cron for posting periodic depreciation
|
||||
|
||||
AI augmentation:
|
||||
- Anomaly detection on utilization vs expected
|
||||
- AI-suggested useful life from invoice context (LLM)
|
||||
|
||||
Coexists with Enterprise: when account_asset is installed, the Fusion
|
||||
menu hides; the engine + AI tools remain available for the chat.
|
||||
""",
|
||||
'author': 'Fusion Accounting',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'fusion_accounting_core',
|
||||
'fusion_accounting_ai',
|
||||
'fusion_accounting_migration',
|
||||
'account',
|
||||
'mail',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron.xml',
|
||||
'wizards/create_asset_wizard_views.xml',
|
||||
'wizards/disposal_wizard_views.xml',
|
||||
'wizards/partial_sale_wizard_views.xml',
|
||||
'wizards/depreciation_run_wizard_views.xml',
|
||||
'reports/migration_audit_report_views.xml',
|
||||
'reports/migration_audit_report_action.xml',
|
||||
'views/menu_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_accounting_assets/static/src/scss/_variables.scss',
|
||||
'fusion_accounting_assets/static/src/scss/assets.scss',
|
||||
'fusion_accounting_assets/static/src/services/assets_service.js',
|
||||
'fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard.js',
|
||||
'fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard.xml',
|
||||
'fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard_view.js',
|
||||
'fusion_accounting_assets/static/src/components/asset_card/asset_card.js',
|
||||
'fusion_accounting_assets/static/src/components/asset_card/asset_card.xml',
|
||||
'fusion_accounting_assets/static/src/components/asset_detail_panel/asset_detail_panel.js',
|
||||
'fusion_accounting_assets/static/src/components/asset_detail_panel/asset_detail_panel.xml',
|
||||
'fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js',
|
||||
'fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.xml',
|
||||
'fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js',
|
||||
'fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.xml',
|
||||
'fusion_accounting_assets/static/src/components/ai_useful_life_panel/ai_useful_life_panel.js',
|
||||
'fusion_accounting_assets/static/src/components/ai_useful_life_panel/ai_useful_life_panel.xml',
|
||||
'fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.js',
|
||||
'fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.xml',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'fusion_accounting_assets/static/src/tours/assets_tours.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'icon': '/fusion_accounting_assets/static/description/icon.png',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import assets_controller
|
||||
@@ -0,0 +1,175 @@
|
||||
"""HTTP controller: 8 JSON-RPC endpoints for the OWL asset dashboard.
|
||||
|
||||
All endpoints route through fusion.asset.engine. V19 type='jsonrpc'.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_date(value):
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if not value:
|
||||
return None
|
||||
return datetime.strptime(value, '%Y-%m-%d').date()
|
||||
|
||||
|
||||
class FusionAssetsController(http.Controller):
|
||||
|
||||
@http.route('/fusion/assets/list', type='jsonrpc', auth='user')
|
||||
def list_assets(self, state=None, category_id=None, limit=50, offset=0,
|
||||
company_id=None):
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
Asset = request.env['fusion.asset'].sudo()
|
||||
domain = [('company_id', '=', company_id)]
|
||||
if state:
|
||||
domain.append(('state', '=', state))
|
||||
if category_id:
|
||||
domain.append(('category_id', '=', int(category_id)))
|
||||
total = Asset.search_count(domain)
|
||||
assets = Asset.search(domain, limit=int(limit), offset=int(offset),
|
||||
order='acquisition_date desc')
|
||||
return {
|
||||
'count': len(assets),
|
||||
'total': total,
|
||||
'assets': [{
|
||||
'id': a.id, 'name': a.name, 'code': a.code or '',
|
||||
'state': a.state, 'cost': a.cost, 'salvage_value': a.salvage_value,
|
||||
'book_value': a.book_value, 'total_depreciated': a.total_depreciated,
|
||||
'method': a.method, 'useful_life_years': a.useful_life_years,
|
||||
'acquisition_date': str(a.acquisition_date),
|
||||
'in_service_date': str(a.in_service_date) if a.in_service_date else None,
|
||||
'category_id': a.category_id.id if a.category_id else None,
|
||||
'category_name': a.category_id.name if a.category_id else None,
|
||||
'currency_code': a.currency_id.name,
|
||||
} for a in assets],
|
||||
}
|
||||
|
||||
@http.route('/fusion/assets/get_detail', type='jsonrpc', auth='user')
|
||||
def get_detail(self, asset_id):
|
||||
asset = request.env['fusion.asset'].browse(int(asset_id))
|
||||
if not asset.exists():
|
||||
raise ValidationError(_("Asset %s not found") % asset_id)
|
||||
return {
|
||||
'asset': {
|
||||
'id': asset.id, 'name': asset.name, 'code': asset.code or '',
|
||||
'state': asset.state, 'cost': asset.cost,
|
||||
'salvage_value': asset.salvage_value,
|
||||
'book_value': asset.book_value,
|
||||
'total_depreciated': asset.total_depreciated,
|
||||
'method': asset.method,
|
||||
'useful_life_years': asset.useful_life_years,
|
||||
'declining_rate_pct': asset.declining_rate_pct,
|
||||
'total_units_expected': asset.total_units_expected,
|
||||
'units_used_to_date': asset.units_used_to_date,
|
||||
'prorate_convention': asset.prorate_convention,
|
||||
'acquisition_date': str(asset.acquisition_date),
|
||||
'in_service_date': str(asset.in_service_date) if asset.in_service_date else None,
|
||||
'disposed_date': str(asset.disposed_date) if asset.disposed_date else None,
|
||||
'category_id': asset.category_id.id if asset.category_id else None,
|
||||
'category_name': asset.category_id.name if asset.category_id else None,
|
||||
'currency_id': asset.currency_id.id,
|
||||
'currency_code': asset.currency_id.name,
|
||||
},
|
||||
'depreciation_lines': [{
|
||||
'id': l.id, 'period_index': l.period_index,
|
||||
'scheduled_date': str(l.scheduled_date),
|
||||
'amount': l.amount, 'accumulated': l.accumulated,
|
||||
'book_value_at_end': l.book_value_at_end,
|
||||
'is_posted': l.is_posted,
|
||||
'posted_date': str(l.posted_date) if l.posted_date else None,
|
||||
} for l in asset.depreciation_line_ids.sorted('period_index')],
|
||||
'anomalies': [{
|
||||
'id': a.id, 'anomaly_type': a.anomaly_type,
|
||||
'severity': a.severity, 'detail': a.detail or '',
|
||||
'state': a.state,
|
||||
} for a in request.env['fusion.asset.anomaly'].search([
|
||||
('asset_id', '=', asset.id), ('state', 'in', ('new', 'acknowledged'))
|
||||
])],
|
||||
}
|
||||
|
||||
@http.route('/fusion/assets/compute_schedule', type='jsonrpc', auth='user')
|
||||
def compute_schedule(self, asset_id, recompute=False):
|
||||
asset = request.env['fusion.asset'].sudo().browse(int(asset_id))
|
||||
engine = request.env['fusion.asset.engine'].sudo()
|
||||
return engine.compute_depreciation_schedule(asset, recompute=bool(recompute))
|
||||
|
||||
@http.route('/fusion/assets/post_depreciation', type='jsonrpc', auth='user')
|
||||
def post_depreciation(self, asset_id, period_date=None):
|
||||
asset = request.env['fusion.asset'].sudo().browse(int(asset_id))
|
||||
engine = request.env['fusion.asset.engine'].sudo()
|
||||
return engine.post_depreciation_entry(asset, period_date=_parse_date(period_date))
|
||||
|
||||
@http.route('/fusion/assets/dispose', type='jsonrpc', auth='user')
|
||||
def dispose(self, asset_id, sale_amount=0, sale_date=None,
|
||||
sale_partner_id=None, disposal_type='sale'):
|
||||
asset = request.env['fusion.asset'].sudo().browse(int(asset_id))
|
||||
engine = request.env['fusion.asset.engine'].sudo()
|
||||
partner = None
|
||||
if sale_partner_id:
|
||||
partner = request.env['res.partner'].sudo().browse(int(sale_partner_id))
|
||||
return engine.dispose_asset(
|
||||
asset, sale_amount=float(sale_amount),
|
||||
sale_date=_parse_date(sale_date),
|
||||
sale_partner=partner, disposal_type=disposal_type,
|
||||
)
|
||||
|
||||
@http.route('/fusion/assets/get_anomalies', type='jsonrpc', auth='user')
|
||||
def get_anomalies(self, asset_id=None, severity=None, state='new', limit=50,
|
||||
company_id=None):
|
||||
company_id = int(company_id) if company_id else request.env.company.id
|
||||
Anomaly = request.env['fusion.asset.anomaly'].sudo()
|
||||
domain = [('company_id', '=', company_id)]
|
||||
if asset_id:
|
||||
domain.append(('asset_id', '=', int(asset_id)))
|
||||
if severity:
|
||||
domain.append(('severity', '=', severity))
|
||||
if state:
|
||||
domain.append(('state', '=', state))
|
||||
anomalies = Anomaly.search(domain, limit=int(limit), order='detected_at desc')
|
||||
return {
|
||||
'count': len(anomalies),
|
||||
'anomalies': [{
|
||||
'id': a.id, 'asset_id': a.asset_id.id, 'asset_name': a.asset_id.name,
|
||||
'anomaly_type': a.anomaly_type, 'severity': a.severity,
|
||||
'expected': a.expected, 'actual': a.actual,
|
||||
'variance_pct': a.variance_pct, 'detail': a.detail or '',
|
||||
'state': a.state,
|
||||
'detected_at': str(a.detected_at),
|
||||
} for a in anomalies],
|
||||
}
|
||||
|
||||
@http.route('/fusion/assets/suggest_useful_life', type='jsonrpc', auth='user')
|
||||
def suggest_useful_life(self, description, amount=None, partner_name=None):
|
||||
from odoo.addons.fusion_accounting_assets.services.useful_life_predictor import (
|
||||
predict_useful_life,
|
||||
)
|
||||
return predict_useful_life(
|
||||
request.env, description=description,
|
||||
amount=float(amount) if amount is not None else None,
|
||||
partner_name=partner_name,
|
||||
)
|
||||
|
||||
@http.route('/fusion/assets/get_partner_history', type='jsonrpc', auth='user')
|
||||
def get_partner_history(self, partner_id, limit=20):
|
||||
Asset = request.env['fusion.asset'].sudo()
|
||||
assets = Asset.search([
|
||||
('source_invoice_line_id.partner_id', '=', int(partner_id)),
|
||||
], limit=int(limit), order='acquisition_date desc')
|
||||
return {
|
||||
'partner_id': int(partner_id),
|
||||
'count': len(assets),
|
||||
'assets': [{
|
||||
'id': a.id, 'name': a.name,
|
||||
'cost': a.cost, 'book_value': a.book_value,
|
||||
'state': a.state,
|
||||
'acquisition_date': str(a.acquisition_date),
|
||||
} for a in assets],
|
||||
}
|
||||
34
fusion_accounting/fusion_accounting_assets/data/cron.xml
Normal file
34
fusion_accounting/fusion_accounting_assets/data/cron.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="cron_fusion_assets_post_depreciation" model="ir.cron">
|
||||
<field name="name">Fusion Assets — Post Due Depreciation</field>
|
||||
<field name="model_id" ref="model_fusion_assets_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_post_due_depreciation()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="cron_fusion_assets_refresh_book_values_mv" model="ir.cron">
|
||||
<field name="name">Fusion Assets — Refresh Book Values MV</field>
|
||||
<field name="model_id" ref="model_fusion_assets_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_refresh_book_values_mv()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="cron_fusion_assets_anomaly_scan" model="ir.cron">
|
||||
<field name="name">Fusion Assets — Monthly Anomaly Scan</field>
|
||||
<field name="model_id" ref="model_fusion_assets_cron"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_anomaly_scan()</field>
|
||||
<field name="interval_number">30</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Materialized view: per-asset book value snapshot.
|
||||
-- Refreshed via cron. Used by the OWL dashboard for portfolio summaries.
|
||||
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS fusion_asset_book_values_mv AS
|
||||
SELECT
|
||||
a.id AS id,
|
||||
a.id AS asset_id,
|
||||
a.company_id,
|
||||
a.category_id,
|
||||
a.state,
|
||||
a.cost,
|
||||
a.salvage_value,
|
||||
COALESCE(SUM(CASE WHEN l.is_posted THEN l.amount ELSE 0 END), 0) AS total_depreciated,
|
||||
a.cost - COALESCE(SUM(CASE WHEN l.is_posted THEN l.amount ELSE 0 END), 0) AS book_value,
|
||||
COUNT(l.id) FILTER (WHERE l.is_posted) AS posted_periods,
|
||||
COUNT(l.id) FILTER (WHERE NOT l.is_posted) AS pending_periods,
|
||||
a.acquisition_date,
|
||||
a.in_service_date
|
||||
FROM fusion_asset a
|
||||
LEFT JOIN fusion_asset_depreciation_line l ON l.asset_id = a.id
|
||||
GROUP BY a.id, a.company_id, a.category_id, a.state, a.cost, a.salvage_value,
|
||||
a.acquisition_date, a.in_service_date;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS fusion_asset_book_values_mv_pkey
|
||||
ON fusion_asset_book_values_mv (id);
|
||||
CREATE INDEX IF NOT EXISTS fusion_asset_book_values_mv_company_state
|
||||
ON fusion_asset_book_values_mv (company_id, state);
|
||||
CREATE INDEX IF NOT EXISTS fusion_asset_book_values_mv_category
|
||||
ON fusion_asset_book_values_mv (category_id) WHERE category_id IS NOT NULL;
|
||||
@@ -0,0 +1,332 @@
|
||||
# Graph Report - /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets (2026-04-22)
|
||||
|
||||
## Corpus Check
|
||||
- 71 files · ~16,325 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 467 nodes · 660 edges · 46 communities detected
|
||||
- Extraction: 73% EXTRACTED · 27% INFERRED · 0% AMBIGUOUS · INFERRED: 179 edges (avg confidence: 0.8)
|
||||
- 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]]
|
||||
- [[_COMMUNITY_Community 38|Community 38]]
|
||||
- [[_COMMUNITY_Community 39|Community 39]]
|
||||
- [[_COMMUNITY_Community 40|Community 40]]
|
||||
- [[_COMMUNITY_Community 41|Community 41]]
|
||||
- [[_COMMUNITY_Community 42|Community 42]]
|
||||
- [[_COMMUNITY_Community 43|Community 43]]
|
||||
- [[_COMMUNITY_Community 44|Community 44]]
|
||||
- [[_COMMUNITY_Community 45|Community 45]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `compute_depreciation_schedule()` - 37 edges
|
||||
2. `TestFusionAssetEngine` - 14 edges
|
||||
3. `post_depreciation_entry()` - 14 edges
|
||||
4. `predict_useful_life()` - 13 edges
|
||||
5. `TestAssetsController` - 12 edges
|
||||
6. `straight_line()` - 11 edges
|
||||
7. `TestFusionAsset` - 10 edges
|
||||
8. `TestAssetAnomalyDetection` - 10 edges
|
||||
9. `TestAssetEngineIntegration` - 10 edges
|
||||
10. `AssetsService` - 10 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `compute_schedule()` --calls--> `compute_depreciation_schedule()` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/controllers/assets_controller.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_engine.py
|
||||
- `suggest_useful_life()` --calls--> `predict_useful_life()` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/controllers/assets_controller.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_predictor.py
|
||||
- `test_straight_line_total_equals_cost_minus_salvage()` --calls--> `straight_line()` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_engine_property.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/depreciation_methods.py
|
||||
- `test_straight_line_book_value_decreasing()` --calls--> `straight_line()` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_engine_property.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/depreciation_methods.py
|
||||
- `test_declining_balance_never_below_salvage()` --calls--> `declining_balance()` [INFERRED]
|
||||
/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_engine_property.py → /Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/depreciation_methods.py
|
||||
|
||||
## Communities
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.09
|
||||
Nodes (15): FusionDepreciationRunWizard, Manual depreciation run wizard. Operator picks a period_date and the wizard pos, compute_depreciation_schedule(), dispose_asset(), partial_sale(), pause_asset(), post_depreciation_entry(), The asset engine — orchestrator for all asset depreciation + lifecycle. 7-metho (+7 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.07
|
||||
Nodes (21): declining_balance(), DepreciationStep, Depreciation method primitives. Three methods supported: - straight_line: equal, Equal charge per period: (cost - salvage) / n_periods. Last period absorbs, Apply `rate` (e.g. 0.20 = 20%) to remaining book each period. Switches to s, Charge per period = (units_used / total_expected) * (cost - salvage)., straight_line(), units_of_production() (+13 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.07
|
||||
Nodes (19): AssetAnomaly, detect_low_utilization(), detect_schedule_variance(), Asset utilization anomaly detection. Flags assets where actual usage / posted d, Compare expected accumulated depreciation vs actual posted., For units-of-production assets: flag low actual usage., FusionAssetBookValuesMV, MV of per-asset book value snapshot. Refresh via cron or model._refresh(). (+11 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.06
|
||||
Nodes (10): AssetsAdapter wiring tests — fusion-mode dispatch., TestAssetsAdapter, TestAuditReport, TestFusionAssetCategory, TestFusionAssetDisposal, Integration tests verifying all 3 depreciation methods through the engine., TestDecliningBalanceIntegration, TestStraightLineIntegration (+2 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.07
|
||||
Nodes (5): AiUsefulLifePanel, AssetDashboard, AssetDetailPanel, AssetsService, DisposalDialog
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.08
|
||||
Nodes (9): HttpCase, Controller tests using HttpCase., TestAssetsController, Python wrappers that run the OWL tours via HttpCase.start_tour. Tours require a, TestAssetsTours, Controller perf benchmarks tagged 'benchmark'. Engine-level benchmarks live in, TestAssetsControllerBenchmarks, Performance benchmarks tagged 'benchmark'. (+1 more)
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.09
|
||||
Nodes (16): _detect_local_llm(), Local LLM compat smoke test for the useful_life_predictor service. Auto-detects, _server_reachable(), TestLocalLLMUsefulLife, TestUsefulLifePredictor, TestUsefulLifePrompt, _get_provider(), predict_useful_life() (+8 more)
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.12
|
||||
Nodes (3): FusionAsset, Fusion Asset model. Lifecycle: draft -> running -> (paused -> running)* -> disp, TestFusionAsset
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.14
|
||||
Nodes (6): FusionCreateAssetWizard, _onchange_category_id(), Create-asset-from-invoice-line wizard. Reads an account.move.line as the source, Create the fusion.asset record + link to source invoice line., Call AI useful-life predictor., TestCreateAssetWizard
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.23
|
||||
Nodes (7): compute_salvage_value(), Salvage value (scrap value) calculation helpers. Most clients use straight % of, Compute end-of-life salvage value., Estimate remaining value if asset is sold/scrapped now., remaining_useful_life_value(), SalvageConfig, TestSalvageValue
|
||||
|
||||
### Community 10 - "Community 10"
|
||||
Cohesion: 0.22
|
||||
Nodes (4): FusionAssetDepreciationLine, Per-period depreciation board lines for an asset., Mark this line as posted (without creating the journal entry yet — engin, TestFusionAssetDepreciationLine
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.21
|
||||
Nodes (3): FusionAssetAnomaly, Persisted asset anomaly flags from the engine's variance detection., TestFusionAssetAnomaly
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.24
|
||||
Nodes (4): prorate_factor(), Prorating helpers for first-period and last-period depreciation. When an asset, Return a 0..1 factor for how much of `period`'s depreciation applies to an a, TestProrate
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.2
|
||||
Nodes (6): FusionMigrationWizard, Assets-specific migration step. Backfills fusion.asset from existing account.as, Backfill fusion.asset from account.asset (Enterprise) if it exists., Override to add assets-bootstrap step., When Enterprise account.asset is NOT installed, step is a no-op., TestAssetsMigrationRoundTrip
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.18
|
||||
Nodes (4): _compute_gain_loss(), FusionDisposalWizard, Asset disposal wizard (sale, scrap, donation, lost)., TestDisposalWizard
|
||||
|
||||
### Community 15 - "Community 15"
|
||||
Cohesion: 0.2
|
||||
Nodes (4): _compute_sold_cost(), FusionPartialSaleWizard, Partial sale wizard (sell a portion of an asset). Splits the asset into a child, TestPartialSaleWizard
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.2
|
||||
Nodes (7): compute_schedule(), dispose(), FusionAssetsController, _parse_date(), post_depreciation(), HTTP controller: 8 JSON-RPC endpoints for the OWL asset dashboard. All endpoint, suggest_useful_life()
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.18
|
||||
Nodes (3): AccountMoveLine, Inherit account.move.line to link to fusion.asset records. Lets us trace assets, TestAccountMoveLineFusionAsset
|
||||
|
||||
### Community 18 - "Community 18"
|
||||
Cohesion: 0.25
|
||||
Nodes (2): Tests for the 5 fusion-asset AI tools., TestFusionAssetTools
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.25
|
||||
Nodes (3): Coexistence tests: fusion_accounting_assets menu only visible when Enterprise ac, Engine is registered regardless of Enterprise install state., TestAssetsCoexistence
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): FusionAssetCategory, Asset categories with default settings (used as templates).
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): FusionAssetDisposal, Asset disposal records (sale, scrap, donation).
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.5
|
||||
Nodes (2): FusionAssetsMigrationAuditReport, QWeb PDF: migration audit report for fusion_accounting_assets.
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.67
|
||||
Nodes (1): AnomalyStrip
|
||||
|
||||
### Community 24 - "Community 24"
|
||||
Cohesion: 0.67
|
||||
Nodes (1): DepreciationBoard
|
||||
|
||||
### Community 25 - "Community 25"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): AssetCard
|
||||
|
||||
### 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 (0):
|
||||
|
||||
### Community 34 - "Community 34"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): For each running asset, post any due un-posted depreciation lines.
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Refresh the per-asset book value MV (hourly).
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): For each running asset, compare expected accumulated depreciation vs pos
|
||||
|
||||
### Community 37 - "Community 37"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Compute (or re-compute) the depreciation board for an asset. If recompu
|
||||
|
||||
### Community 38 - "Community 38"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Post the next-due un-posted depreciation line. If period_date provided,
|
||||
|
||||
### Community 39 - "Community 39"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Dispose an asset (sale, scrap, donation, lost).
|
||||
|
||||
### Community 40 - "Community 40"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Partially dispose: split asset into two — sold child + remaining parent.
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Pause depreciation. Wraps asset.action_pause for API symmetry and to log
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Resume a paused asset.
|
||||
|
||||
### Community 43 - "Community 43"
|
||||
Cohesion: 1.0
|
||||
Nodes (1): Reverse a disposal (rare — recovery from accidental sale entry).
|
||||
|
||||
### Community 44 - "Community 44"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
### Community 45 - "Community 45"
|
||||
Cohesion: 1.0
|
||||
Nodes (0):
|
||||
|
||||
## Knowledge Gaps
|
||||
- **72 isolated node(s):** `Integration tests verifying all 3 depreciation methods through the engine.`, `Property-based invariant tests for the asset engine. Hypothesis generates rando`, `Local LLM compat smoke test for the useful_life_predictor service. Auto-detects`, `Python wrappers that run the OWL tours via HttpCase.start_tour. Tours require a`, `Tests for the per-asset book value MV.` (+67 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **Thin community `Community 25`** (2 nodes): `AssetCard`, `asset_card.js`
|
||||
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): `__init__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 33`** (1 nodes): `__manifest__.py`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 34`** (1 nodes): `For each running asset, post any due un-posted depreciation lines.`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 35`** (1 nodes): `Refresh the per-asset book value MV (hourly).`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 36`** (1 nodes): `For each running asset, compare expected accumulated depreciation vs pos`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 37`** (1 nodes): `Compute (or re-compute) the depreciation board for an asset. If recompu`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 38`** (1 nodes): `Post the next-due un-posted depreciation line. If period_date provided,`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 39`** (1 nodes): `Dispose an asset (sale, scrap, donation, lost).`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 40`** (1 nodes): `Partially dispose: split asset into two — sold child + remaining parent.`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 41`** (1 nodes): `Pause depreciation. Wraps asset.action_pause for API symmetry and to log`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 42`** (1 nodes): `Resume a paused asset.`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 43`** (1 nodes): `Reverse a disposal (rare — recovery from accidental sale entry).`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 44`** (1 nodes): `assets_tours.js`
|
||||
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||
- **Thin community `Community 45`** (1 nodes): `asset_dashboard_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 `compute_depreciation_schedule()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 5`, `Community 14`, `Community 15`, `Community 16`?**
|
||||
_High betweenness centrality (0.101) - this node is a cross-community bridge._
|
||||
- **Why does `TestEngineBenchmarks` connect `Community 0` to `Community 3`, `Community 5`?**
|
||||
_High betweenness centrality (0.057) - this node is a cross-community bridge._
|
||||
- **Why does `TestFusionAssetEngine` connect `Community 0` to `Community 3`?**
|
||||
_High betweenness centrality (0.052) - this node is a cross-community bridge._
|
||||
- **Are the 32 inferred relationships involving `compute_depreciation_schedule()` (e.g. with `.test_straight_line_5yr_no_salvage()` and `.test_straight_line_10yr_with_salvage()`) actually correct?**
|
||||
_`compute_depreciation_schedule()` has 32 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 12 inferred relationships involving `post_depreciation_entry()` (e.g. with `.test_post_depreciation_entry_marks_line_posted()` and `.test_post_depreciation_only_after_running()`) actually correct?**
|
||||
_`post_depreciation_entry()` has 12 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Are the 9 inferred relationships involving `predict_useful_life()` (e.g. with `.test_useful_life_with_local_llm()` and `.test_fallback_computer()`) actually correct?**
|
||||
_`predict_useful_life()` has 9 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `Integration tests verifying all 3 depreciation methods through the engine.`, `Property-based invariant tests for the asset engine. Hypothesis generates rando`, `Local LLM compat smoke test for the useful_life_predictor service. Auto-detects` to the rest of the system?**
|
||||
_72 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_assets_controllers_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/controllers/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_controllers_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_controllers_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/controllers/__init__.py", "source_location": "L1", "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_assets_models_account_move_py", "label": "account_move.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L1"}, {"id": "account_move_accountmoveline", "label": "AccountMoveLine", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L9"}, {"id": "account_move_accountmoveline_compute_fusion_asset_count", "label": "._compute_fusion_asset_count()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L20"}, {"id": "account_move_accountmoveline_action_open_fusion_asset", "label": ".action_open_fusion_asset()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L24"}, {"id": "account_move_rationale_1", "label": "Inherit account.move.line to link to fusion.asset records. Lets us trace assets", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_account_move_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_account_move_py", "target": "account_move_accountmoveline", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L9", "weight": 1.0}, {"source": "account_move_accountmoveline", "target": "account_move_accountmoveline_compute_fusion_asset_count", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L20", "weight": 1.0}, {"source": "account_move_accountmoveline", "target": "account_move_accountmoveline_action_open_fusion_asset", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L24", "weight": 1.0}, {"source": "account_move_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_account_move_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "account_move_accountmoveline_action_open_fusion_asset", "callee": "ensure_one", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/account_move.py", "source_location": "L25"}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_disposal_dialog_disposal_dialog_js", "label": "disposal_dialog.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L1"}, {"id": "disposal_dialog_disposaldialog", "label": "DisposalDialog", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L6"}, {"id": "disposal_dialog_disposaldialog_setup", "label": ".setup()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L13"}, {"id": "disposal_dialog_disposaldialog_onconfirm", "label": ".onConfirm()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L22"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_disposal_dialog_disposal_dialog_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_disposal_dialog_disposal_dialog_js", "target": "hooks", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_disposal_dialog_disposal_dialog_js", "target": "disposal_dialog_disposaldialog", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L6", "weight": 1.0}, {"source": "disposal_dialog_disposaldialog", "target": "disposal_dialog_disposaldialog_setup", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L13", "weight": 1.0}, {"source": "disposal_dialog_disposaldialog", "target": "disposal_dialog_disposaldialog_onconfirm", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L22", "weight": 1.0}], "raw_calls": [{"caller_nid": "disposal_dialog_disposaldialog_setup", "callee": "useService", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L14"}, {"caller_nid": "disposal_dialog_disposaldialog_setup", "callee": "useState", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L15"}, {"caller_nid": "disposal_dialog_disposaldialog_setup", "callee": "slice", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L18"}, {"caller_nid": "disposal_dialog_disposaldialog_setup", "callee": "toISOString", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L18"}, {"caller_nid": "disposal_dialog_disposaldialog_onconfirm", "callee": "disposeAsset", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L24"}, {"caller_nid": "disposal_dialog_disposaldialog_onconfirm", "callee": "parseFloat", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L26"}, {"caller_nid": "disposal_dialog_disposaldialog_onconfirm", "callee": "onClose", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/disposal_dialog/disposal_dialog.js", "source_location": "L29"}]}
|
||||
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_assets_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_migration_audit_report_py", "label": "migration_audit_report.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L1"}, {"id": "migration_audit_report_fusionassetsmigrationauditreport", "label": "FusionAssetsMigrationAuditReport", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L6"}, {"id": "migration_audit_report_get_report_values", "label": "_get_report_values()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L11"}, {"id": "migration_audit_report_rationale_1", "label": "QWeb PDF: migration audit report for fusion_accounting_assets.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_migration_audit_report_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_migration_audit_report_py", "target": "migration_audit_report_fusionassetsmigrationauditreport", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_migration_audit_report_py", "target": "migration_audit_report_get_report_values", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L11", "weight": 1.0}, {"source": "migration_audit_report_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_migration_audit_report_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "migration_audit_report_get_report_values", "callee": "browse", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L12"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "search", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L15"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "search", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L16"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "sum", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L19"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "sum", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L20"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "sum", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L21"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "sum", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L22"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "append", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L23"}, {"caller_nid": "migration_audit_report_get_report_values", "callee": "len", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/migration_audit_report.py", "source_location": "L25"}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/__init__.py", "source_location": "L5", "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_assets_reports_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/reports/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_reports_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/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_assets_static_src_components_depreciation_board_depreciation_board_js", "label": "depreciation_board.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js", "source_location": "L1"}, {"id": "depreciation_board_depreciationboard", "label": "DepreciationBoard", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js", "source_location": "L5"}, {"id": "depreciation_board_depreciationboard_rowclass", "label": ".rowClass()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js", "source_location": "L12"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_depreciation_board_depreciation_board_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_depreciation_board_depreciation_board_js", "target": "depreciation_board_depreciationboard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js", "source_location": "L5", "weight": 1.0}, {"source": "depreciation_board_depreciationboard", "target": "depreciation_board_depreciationboard_rowclass", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/depreciation_board/depreciation_board.js", "source_location": "L12", "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
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L9", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/__init__.py", "source_location": "L10", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/__init__.py", "source_location": "L6", "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_assets_models_fusion_asset_disposal_py", "label": "fusion_asset_disposal.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L1"}, {"id": "fusion_asset_disposal_fusionassetdisposal", "label": "FusionAssetDisposal", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L14"}, {"id": "fusion_asset_disposal_compute_gain_loss", "label": "_compute_gain_loss()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L51"}, {"id": "fusion_asset_disposal_rationale_1", "label": "Asset disposal records (sale, scrap, donation).", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_disposal_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_disposal_py", "target": "fusion_asset_disposal_fusionassetdisposal", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L14", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_disposal_py", "target": "fusion_asset_disposal_compute_gain_loss", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L51", "weight": 1.0}, {"source": "fusion_asset_disposal_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_disposal_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_disposal.py", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/wizards/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/wizards/__init__.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/wizards/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/wizards/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_wizards_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/wizards/__init__.py", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_category_py", "label": "fusion_asset_category.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L1"}, {"id": "fusion_asset_category_fusionassetcategory", "label": "FusionAssetCategory", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L6"}, {"id": "fusion_asset_category_fusionassetcategory_compute_asset_count", "label": "._compute_asset_count()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L49"}, {"id": "fusion_asset_category_rationale_1", "label": "Asset categories with default settings (used as templates).", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_category_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_category_py", "target": "fusion_asset_category_fusionassetcategory", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L6", "weight": 1.0}, {"source": "fusion_asset_category_fusionassetcategory", "target": "fusion_asset_category_fusionassetcategory_compute_asset_count", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L49", "weight": 1.0}, {"source": "fusion_asset_category_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_category_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "fusion_asset_category_fusionassetcategory_compute_asset_count", "callee": "search_count", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_category.py", "source_location": "L51"}]}
|
||||
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
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_assets_static_src_components_anomaly_strip_anomaly_strip_js", "label": "anomaly_strip.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/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_assets/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L5"}, {"id": "anomaly_strip_anomalystrip_formatnumber", "label": ".formatNumber()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L11"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_anomaly_strip_anomaly_strip_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_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_assets/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L5", "weight": 1.0}, {"source": "anomaly_strip_anomalystrip", "target": "anomaly_strip_anomalystrip_formatnumber", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L11", "weight": 1.0}], "raw_calls": [{"caller_nid": "anomaly_strip_anomalystrip_formatnumber", "callee": "format", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/anomaly_strip/anomaly_strip.js", "source_location": "L13"}]}
|
||||
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_assets_models_fusion_asset_depreciation_line_py", "label": "fusion_asset_depreciation_line.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L1"}, {"id": "fusion_asset_depreciation_line_fusionassetdepreciationline", "label": "FusionAssetDepreciationLine", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L6"}, {"id": "fusion_asset_depreciation_line_fusionassetdepreciationline_action_post", "label": ".action_post()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L28"}, {"id": "fusion_asset_depreciation_line_rationale_1", "label": "Per-period depreciation board lines for an asset.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L1"}, {"id": "fusion_asset_depreciation_line_rationale_29", "label": "Mark this line as posted (without creating the journal entry yet \u2014 engin", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L29"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_depreciation_line_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_depreciation_line_py", "target": "fusion_asset_depreciation_line_fusionassetdepreciationline", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L6", "weight": 1.0}, {"source": "fusion_asset_depreciation_line_fusionassetdepreciationline", "target": "fusion_asset_depreciation_line_fusionassetdepreciationline_action_post", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L28", "weight": 1.0}, {"source": "fusion_asset_depreciation_line_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_depreciation_line_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L1", "weight": 1.0}, {"source": "fusion_asset_depreciation_line_rationale_29", "target": "fusion_asset_depreciation_line_fusionassetdepreciationline_action_post", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L29", "weight": 1.0}], "raw_calls": [{"caller_nid": "fusion_asset_depreciation_line_fusionassetdepreciationline_action_post", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L34"}, {"caller_nid": "fusion_asset_depreciation_line_fusionassetdepreciationline_action_post", "callee": "today", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_depreciation_line.py", "source_location": "L36"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_useful_life_prompt_py", "label": "useful_life_prompt.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L1"}, {"id": "useful_life_prompt_build_prompt", "label": "build_prompt()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L38"}, {"id": "useful_life_prompt_rationale_1", "label": "LLM prompt builder for AI-suggested useful life from invoice description. Outpu", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L1"}, {"id": "useful_life_prompt_rationale_40", "label": "Return (system, user) prompt tuple.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L40"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_useful_life_prompt_py", "target": "useful_life_prompt_build_prompt", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L38", "weight": 1.0}, {"source": "useful_life_prompt_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_useful_life_prompt_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L1", "weight": 1.0}, {"source": "useful_life_prompt_rationale_40", "target": "useful_life_prompt_build_prompt", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L40", "weight": 1.0}], "raw_calls": [{"caller_nid": "useful_life_prompt_build_prompt", "callee": "append", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L43"}, {"caller_nid": "useful_life_prompt_build_prompt", "callee": "append", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L45"}, {"caller_nid": "useful_life_prompt_build_prompt", "callee": "append", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L46"}, {"caller_nid": "useful_life_prompt_build_prompt", "callee": "append", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L47"}, {"caller_nid": "useful_life_prompt_build_prompt", "callee": "join", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/useful_life_prompt.py", "source_location": "L48"}]}
|
||||
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_assets_models_fusion_asset_anomaly_py", "label": "fusion_asset_anomaly.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L1"}, {"id": "fusion_asset_anomaly_fusionassetanomaly", "label": "FusionAssetAnomaly", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L14"}, {"id": "fusion_asset_anomaly_fusionassetanomaly_action_acknowledge", "label": ".action_acknowledge()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L35"}, {"id": "fusion_asset_anomaly_fusionassetanomaly_action_dismiss", "label": ".action_dismiss()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L38"}, {"id": "fusion_asset_anomaly_fusionassetanomaly_action_resolve", "label": ".action_resolve()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L41"}, {"id": "fusion_asset_anomaly_rationale_1", "label": "Persisted asset anomaly flags from the engine's variance detection.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_anomaly_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_anomaly_py", "target": "fusion_asset_anomaly_fusionassetanomaly", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L14", "weight": 1.0}, {"source": "fusion_asset_anomaly_fusionassetanomaly", "target": "fusion_asset_anomaly_fusionassetanomaly_action_acknowledge", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L35", "weight": 1.0}, {"source": "fusion_asset_anomaly_fusionassetanomaly", "target": "fusion_asset_anomaly_fusionassetanomaly_action_dismiss", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L38", "weight": 1.0}, {"source": "fusion_asset_anomaly_fusionassetanomaly", "target": "fusion_asset_anomaly_fusionassetanomaly_action_resolve", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L41", "weight": 1.0}, {"source": "fusion_asset_anomaly_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_models_fusion_asset_anomaly_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L1", "weight": 1.0}], "raw_calls": [{"caller_nid": "fusion_asset_anomaly_fusionassetanomaly_action_acknowledge", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L36"}, {"caller_nid": "fusion_asset_anomaly_fusionassetanomaly_action_dismiss", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L39"}, {"caller_nid": "fusion_asset_anomaly_fusionassetanomaly_action_resolve", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/models/fusion_asset_anomaly.py", "source_location": "L42"}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_prorate_py", "label": "prorate.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L1"}, {"id": "prorate_prorate_factor", "label": "prorate_factor()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L17"}, {"id": "prorate_rationale_1", "label": "Prorating helpers for first-period and last-period depreciation. When an asset", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L1"}, {"id": "prorate_rationale_20", "label": "Return a 0..1 factor for how much of `period`'s depreciation applies to an a", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L20"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_prorate_py", "target": "datetime", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L10", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_prorate_py", "target": "typing", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L11", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_prorate_py", "target": "prorate_prorate_factor", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L17", "weight": 1.0}, {"source": "prorate_rationale_1", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_services_prorate_py", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L1", "weight": 1.0}, {"source": "prorate_rationale_20", "target": "prorate_prorate_factor", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L20", "weight": 1.0}], "raw_calls": [{"caller_nid": "prorate_prorate_factor", "callee": "ValueError", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/services/prorate.py", "source_location": "L34"}]}
|
||||
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_assets_static_src_views_asset_dashboard_asset_dashboard_view_js", "label": "asset_dashboard_view.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard_view.js", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_views_asset_dashboard_asset_dashboard_view_js", "target": "registry", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard_view.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_views_asset_dashboard_asset_dashboard_view_js", "target": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_views_asset_dashboard_asset_dashboard", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/views/asset_dashboard/asset_dashboard_view.js", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_tests_test_audit_report_py", "label": "test_audit_report.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L1"}, {"id": "test_audit_report_testauditreport", "label": "TestAuditReport", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L6"}, {"id": "transactioncase", "label": "TransactionCase", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "test_audit_report_testauditreport_test_report_renders", "label": ".test_report_renders()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L8"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_tests_test_audit_report_py", "target": "odoo_tests_common", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L1", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_tests_test_audit_report_py", "target": "odoo_tests", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_tests_test_audit_report_py", "target": "test_audit_report_testauditreport", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L6", "weight": 1.0}, {"source": "test_audit_report_testauditreport", "target": "transactioncase", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L6", "weight": 1.0}, {"source": "test_audit_report_testauditreport", "target": "test_audit_report_testauditreport_test_report_renders", "relation": "method", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L8", "weight": 1.0}], "raw_calls": [{"caller_nid": "test_audit_report_testauditreport_test_report_renders", "callee": "create", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L9"}, {"caller_nid": "test_audit_report_testauditreport_test_report_renders", "callee": "_render_qweb_pdf", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L11"}, {"caller_nid": "test_audit_report_testauditreport_test_report_renders", "callee": "sudo", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L11"}, {"caller_nid": "test_audit_report_testauditreport_test_report_renders", "callee": "assertGreater", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L16"}, {"caller_nid": "test_audit_report_testauditreport_test_report_renders", "callee": "len", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L16"}, {"caller_nid": "test_audit_report_testauditreport_test_report_renders", "callee": "skipTest", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/tests/test_audit_report.py", "source_location": "L18"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_tours_assets_tours_js", "label": "assets_tours.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/tours/assets_tours.js", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_tours_assets_tours_js", "target": "registry", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/tours/assets_tours.js", "source_location": "L3", "weight": 1.0}], "raw_calls": []}
|
||||
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_asset_card_asset_card_js", "label": "asset_card.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/asset_card/asset_card.js", "source_location": "L1"}, {"id": "asset_card_assetcard", "label": "AssetCard", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/asset_card/asset_card.js", "source_location": "L5"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_asset_card_asset_card_js", "target": "owl", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/asset_card/asset_card.js", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_fusion_accounting_assets_static_src_components_asset_card_asset_card_js", "target": "asset_card_assetcard", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/fusion_accounting_assets/static/src/components/asset_card/asset_card.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
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
12133
fusion_accounting/fusion_accounting_assets/graphify-out/graph.json
Normal file
12133
fusion_accounting/fusion_accounting_assets/graphify-out/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
from . import fusion_asset_category
|
||||
from . import fusion_asset
|
||||
from . import fusion_asset_depreciation_line
|
||||
from . import fusion_asset_disposal
|
||||
from . import fusion_asset_anomaly
|
||||
from . import account_move
|
||||
from . import fusion_asset_engine
|
||||
from . import fusion_assets_cron
|
||||
from . import fusion_asset_book_values_mv
|
||||
from . import fusion_migration_wizard
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Inherit account.move.line to link to fusion.asset records.
|
||||
|
||||
Lets us trace assets back to their source invoice line.
|
||||
"""
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
fusion_asset_id = fields.Many2one(
|
||||
'fusion.asset', string='Created Asset',
|
||||
copy=False, ondelete='set null',
|
||||
help="Fusion asset record created from this invoice line.",
|
||||
)
|
||||
|
||||
fusion_asset_count = fields.Integer(compute='_compute_fusion_asset_count')
|
||||
|
||||
def _compute_fusion_asset_count(self):
|
||||
for line in self:
|
||||
line.fusion_asset_count = 1 if line.fusion_asset_id else 0
|
||||
|
||||
def action_open_fusion_asset(self):
|
||||
self.ensure_one()
|
||||
if not self.fusion_asset_id:
|
||||
return
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.asset',
|
||||
'res_id': self.fusion_asset_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Fusion Asset model.
|
||||
|
||||
Lifecycle: draft -> running -> (paused -> running)* -> disposed.
|
||||
- draft: created, not yet running depreciation
|
||||
- running: depreciation board active, periodic posts happen
|
||||
- paused: depreciation suspended (e.g. asset out for repair)
|
||||
- disposed: sold/scrapped/donated; no further depreciation
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
METHOD_SELECTION = [
|
||||
('straight_line', 'Straight Line'),
|
||||
('declining_balance', 'Declining Balance'),
|
||||
('units_of_production', 'Units of Production'),
|
||||
]
|
||||
|
||||
PRORATE_SELECTION = [
|
||||
('full_month', 'Full Month'),
|
||||
('days_365', 'Days / 365'),
|
||||
('days_period', 'Days in Period'),
|
||||
]
|
||||
|
||||
STATE_SELECTION = [
|
||||
('draft', 'Draft'),
|
||||
('running', 'Running'),
|
||||
('paused', 'Paused'),
|
||||
('disposed', 'Disposed'),
|
||||
]
|
||||
|
||||
|
||||
class FusionAsset(models.Model):
|
||||
_name = "fusion.asset"
|
||||
_description = "Fusion Fixed Asset"
|
||||
_order = "acquisition_date desc, id desc"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
name = fields.Char(required=True, tracking=True)
|
||||
code = fields.Char(help="Internal asset code (e.g. tag number).")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', required=True,
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
category_id = fields.Many2one('fusion.asset.category', tracking=True)
|
||||
state = fields.Selection(
|
||||
STATE_SELECTION, default='draft', required=True, tracking=True,
|
||||
)
|
||||
|
||||
cost = fields.Monetary(
|
||||
required=True, tracking=True,
|
||||
help="Original acquisition cost.",
|
||||
)
|
||||
salvage_value = fields.Monetary(
|
||||
default=0.0, tracking=True,
|
||||
help="Estimated end-of-life value.",
|
||||
)
|
||||
acquisition_date = fields.Date(
|
||||
required=True, default=fields.Date.today, tracking=True,
|
||||
)
|
||||
in_service_date = fields.Date(
|
||||
tracking=True,
|
||||
help="Date depreciation actually begins.",
|
||||
)
|
||||
disposed_date = fields.Date(readonly=True, tracking=True)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', required=True,
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
)
|
||||
|
||||
method = fields.Selection(
|
||||
METHOD_SELECTION, required=True, default='straight_line', tracking=True,
|
||||
)
|
||||
useful_life_years = fields.Integer(
|
||||
default=5, tracking=True,
|
||||
help="For straight_line / declining_balance.",
|
||||
)
|
||||
declining_rate_pct = fields.Float(
|
||||
default=20.0,
|
||||
help="For declining_balance method, e.g. 20.0 = 20%/year.",
|
||||
)
|
||||
total_units_expected = fields.Float(
|
||||
help="For units_of_production method.",
|
||||
)
|
||||
units_used_to_date = fields.Float(
|
||||
default=0.0,
|
||||
help="For units_of_production: track usage.",
|
||||
)
|
||||
prorate_convention = fields.Selection(
|
||||
PRORATE_SELECTION, default='days_period', required=True,
|
||||
)
|
||||
|
||||
source_invoice_line_id = fields.Many2one(
|
||||
'account.move.line', string='Source Invoice Line',
|
||||
help="The invoice line that originated this asset.",
|
||||
)
|
||||
parent_id = fields.Many2one(
|
||||
'fusion.asset', help='For partial-sale child assets.',
|
||||
)
|
||||
|
||||
depreciation_line_ids = fields.One2many(
|
||||
'fusion.asset.depreciation.line', 'asset_id',
|
||||
string='Depreciation Lines',
|
||||
)
|
||||
book_value = fields.Monetary(compute='_compute_book_value', store=True)
|
||||
total_depreciated = fields.Monetary(compute='_compute_book_value', store=True)
|
||||
last_posted_date = fields.Date(compute='_compute_last_posted_date', store=True)
|
||||
|
||||
@api.depends('cost', 'depreciation_line_ids.amount', 'depreciation_line_ids.is_posted')
|
||||
def _compute_book_value(self):
|
||||
for asset in self:
|
||||
posted = sum(l.amount for l in asset.depreciation_line_ids if l.is_posted)
|
||||
asset.total_depreciated = posted
|
||||
asset.book_value = asset.cost - posted
|
||||
|
||||
@api.depends('depreciation_line_ids.is_posted', 'depreciation_line_ids.scheduled_date')
|
||||
def _compute_last_posted_date(self):
|
||||
for asset in self:
|
||||
posted_dates = [
|
||||
l.scheduled_date for l in asset.depreciation_line_ids if l.is_posted
|
||||
]
|
||||
asset.last_posted_date = max(posted_dates) if posted_dates else False
|
||||
|
||||
def action_set_running(self):
|
||||
for asset in self:
|
||||
if asset.state != 'draft':
|
||||
raise ValidationError(_("Only draft assets can be set running."))
|
||||
if not asset.in_service_date:
|
||||
asset.in_service_date = fields.Date.today()
|
||||
asset.state = 'running'
|
||||
|
||||
def action_pause(self):
|
||||
for asset in self:
|
||||
if asset.state != 'running':
|
||||
raise ValidationError(_("Only running assets can be paused."))
|
||||
asset.state = 'paused'
|
||||
|
||||
def action_resume(self):
|
||||
for asset in self:
|
||||
if asset.state != 'paused':
|
||||
raise ValidationError(_("Only paused assets can be resumed."))
|
||||
asset.state = 'running'
|
||||
|
||||
def action_set_draft(self):
|
||||
for asset in self:
|
||||
if asset.state not in ('draft', 'paused'):
|
||||
raise ValidationError(
|
||||
_("Cannot reset to draft from %s.") % asset.state,
|
||||
)
|
||||
asset.state = 'draft'
|
||||
|
||||
_check_cost_positive = models.Constraint(
|
||||
'CHECK(cost >= 0)',
|
||||
'Asset cost must be non-negative.',
|
||||
)
|
||||
_check_salvage_lte_cost = models.Constraint(
|
||||
'CHECK(salvage_value >= 0 AND salvage_value <= cost)',
|
||||
'Salvage value must be between 0 and cost.',
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Persisted asset anomaly flags from the engine's variance detection."""
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
SEVERITY = [('low', 'Low'), ('medium', 'Medium'), ('high', 'High')]
|
||||
ANOMALY_TYPES = [
|
||||
('behind_schedule', 'Behind Schedule'),
|
||||
('ahead_of_schedule', 'Ahead of Schedule'),
|
||||
('low_utilization', 'Low Utilization'),
|
||||
]
|
||||
|
||||
|
||||
class FusionAssetAnomaly(models.Model):
|
||||
_name = "fusion.asset.anomaly"
|
||||
_description = "Flagged Asset Anomaly"
|
||||
_order = "detected_at desc, severity desc"
|
||||
|
||||
asset_id = fields.Many2one('fusion.asset', required=True, ondelete='cascade')
|
||||
company_id = fields.Many2one(related='asset_id.company_id', store=True)
|
||||
anomaly_type = fields.Selection(ANOMALY_TYPES, required=True)
|
||||
severity = fields.Selection(SEVERITY, required=True)
|
||||
expected = fields.Float()
|
||||
actual = fields.Float()
|
||||
variance_pct = fields.Float()
|
||||
detail = fields.Text()
|
||||
detected_at = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||
state = fields.Selection([
|
||||
('new', 'New'),
|
||||
('acknowledged', 'Acknowledged'),
|
||||
('resolved', 'Resolved'),
|
||||
('dismissed', 'Dismissed'),
|
||||
], default='new', required=True)
|
||||
|
||||
def action_acknowledge(self):
|
||||
self.write({'state': 'acknowledged'})
|
||||
|
||||
def action_dismiss(self):
|
||||
self.write({'state': 'dismissed'})
|
||||
|
||||
def action_resolve(self):
|
||||
self.write({'state': 'resolved'})
|
||||
@@ -0,0 +1,59 @@
|
||||
"""MV of per-asset book value snapshot. Refresh via cron or model._refresh()."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionAssetBookValuesMV(models.Model):
|
||||
_name = "fusion.asset.book.values.mv"
|
||||
_description = "MV of asset book value snapshot"
|
||||
_auto = False
|
||||
_table = "fusion_asset_book_values_mv"
|
||||
_order = "book_value desc"
|
||||
|
||||
asset_id = fields.Many2one('fusion.asset', readonly=True)
|
||||
company_id = fields.Many2one('res.company', readonly=True)
|
||||
category_id = fields.Many2one('fusion.asset.category', readonly=True)
|
||||
state = fields.Char(readonly=True)
|
||||
cost = fields.Float(readonly=True)
|
||||
salvage_value = fields.Float(readonly=True)
|
||||
total_depreciated = fields.Float(readonly=True)
|
||||
book_value = fields.Float(readonly=True)
|
||||
posted_periods = fields.Integer(readonly=True)
|
||||
pending_periods = fields.Integer(readonly=True)
|
||||
acquisition_date = fields.Date(readonly=True)
|
||||
in_service_date = fields.Date(readonly=True)
|
||||
|
||||
def init(self):
|
||||
sql_path = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'data', 'sql',
|
||||
'create_mv_asset_book_values.sql',
|
||||
)
|
||||
with open(sql_path, 'r') as f:
|
||||
self.env.cr.execute(f.read())
|
||||
_logger.info("fusion_asset_book_values_mv: created/verified MV")
|
||||
|
||||
@api.model
|
||||
def _refresh(self, *, concurrently=True):
|
||||
# CONCURRENTLY requires a unique index (we have one) and that the MV
|
||||
# has been populated at least once. Wrap the concurrent attempt in a
|
||||
# savepoint so a failure (e.g. first-ever refresh before the MV is
|
||||
# populated) does NOT poison the surrounding transaction; we then
|
||||
# fall back to a plain REFRESH.
|
||||
if concurrently:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
self.env.cr.execute(
|
||||
"REFRESH MATERIALIZED VIEW CONCURRENTLY "
|
||||
"fusion_asset_book_values_mv"
|
||||
)
|
||||
return
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning("Concurrent MV refresh failed (%s); fallback", e)
|
||||
self.env.cr.execute(
|
||||
"REFRESH MATERIALIZED VIEW fusion_asset_book_values_mv"
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Asset categories with default settings (used as templates)."""
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class FusionAssetCategory(models.Model):
|
||||
_name = "fusion.asset.category"
|
||||
_description = "Fusion Asset Category"
|
||||
_order = "sequence, name"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', default=lambda self: self.env.company,
|
||||
)
|
||||
|
||||
method = fields.Selection([
|
||||
('straight_line', 'Straight Line'),
|
||||
('declining_balance', 'Declining Balance'),
|
||||
('units_of_production', 'Units of Production'),
|
||||
], default='straight_line', required=True)
|
||||
useful_life_years = fields.Integer(default=5)
|
||||
declining_rate_pct = fields.Float(default=20.0)
|
||||
salvage_value_pct = fields.Float(
|
||||
default=0.0,
|
||||
help="% of cost (used for new assets in this category).",
|
||||
)
|
||||
prorate_convention = fields.Selection([
|
||||
('full_month', 'Full Month'),
|
||||
('days_365', 'Days / 365'),
|
||||
('days_period', 'Days in Period'),
|
||||
], default='days_period', required=True)
|
||||
|
||||
asset_account_id = fields.Many2one(
|
||||
'account.account', string='Asset Account',
|
||||
domain="[('account_type', 'in', ('asset_fixed', 'asset_non_current'))]",
|
||||
)
|
||||
depreciation_account_id = fields.Many2one(
|
||||
'account.account', string='Depreciation Account',
|
||||
domain="[('account_type', '=', 'asset_fixed')]",
|
||||
)
|
||||
expense_account_id = fields.Many2one(
|
||||
'account.account', string='Expense Account',
|
||||
domain="[('account_type', '=', 'expense_depreciation')]",
|
||||
)
|
||||
|
||||
asset_count = fields.Integer(compute='_compute_asset_count')
|
||||
|
||||
def _compute_asset_count(self):
|
||||
for cat in self:
|
||||
cat.asset_count = self.env['fusion.asset'].search_count([
|
||||
('category_id', '=', cat.id),
|
||||
])
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Per-period depreciation board lines for an asset."""
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FusionAssetDepreciationLine(models.Model):
|
||||
_name = "fusion.asset.depreciation.line"
|
||||
_description = "Asset Depreciation Board Line"
|
||||
_order = "asset_id, scheduled_date"
|
||||
|
||||
asset_id = fields.Many2one('fusion.asset', required=True, ondelete='cascade')
|
||||
company_id = fields.Many2one(related='asset_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='asset_id.currency_id', store=True)
|
||||
|
||||
period_index = fields.Integer(required=True)
|
||||
scheduled_date = fields.Date(required=True)
|
||||
amount = fields.Monetary(required=True)
|
||||
accumulated = fields.Monetary()
|
||||
book_value_at_end = fields.Monetary()
|
||||
|
||||
is_posted = fields.Boolean(default=False, copy=False)
|
||||
posted_date = fields.Date(copy=False)
|
||||
move_id = fields.Many2one(
|
||||
'account.move', copy=False,
|
||||
help="Journal entry created when this line was posted.",
|
||||
)
|
||||
|
||||
def action_post(self):
|
||||
"""Mark this line as posted (without creating the journal entry yet —
|
||||
engine method post_depreciation_entry handles the actual entry creation)."""
|
||||
for line in self:
|
||||
if line.is_posted:
|
||||
continue
|
||||
line.write({
|
||||
'is_posted': True,
|
||||
'posted_date': fields.Date.today(),
|
||||
})
|
||||
|
||||
_unique_period_per_asset = models.Constraint(
|
||||
'UNIQUE(asset_id, period_index)',
|
||||
'A depreciation line for that period already exists.',
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Asset disposal records (sale, scrap, donation)."""
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
DISPOSAL_TYPES = [
|
||||
('sale', 'Sale'),
|
||||
('scrap', 'Scrap'),
|
||||
('donation', 'Donation'),
|
||||
('lost', 'Lost / Stolen'),
|
||||
]
|
||||
|
||||
|
||||
class FusionAssetDisposal(models.Model):
|
||||
_name = "fusion.asset.disposal"
|
||||
_description = "Asset Disposal Record"
|
||||
_order = "disposal_date desc, id desc"
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
asset_id = fields.Many2one(
|
||||
'fusion.asset', required=True, ondelete='restrict', tracking=True,
|
||||
)
|
||||
company_id = fields.Many2one(related='asset_id.company_id', store=True)
|
||||
currency_id = fields.Many2one(related='asset_id.currency_id', store=True)
|
||||
|
||||
disposal_type = fields.Selection(
|
||||
DISPOSAL_TYPES, required=True, default='sale', tracking=True,
|
||||
)
|
||||
disposal_date = fields.Date(
|
||||
required=True, default=fields.Date.today, tracking=True,
|
||||
)
|
||||
sale_amount = fields.Monetary(
|
||||
default=0.0, tracking=True,
|
||||
help="Cash received (for sale disposal type).",
|
||||
)
|
||||
sale_partner_id = fields.Many2one('res.partner', tracking=True)
|
||||
|
||||
book_value_at_disposal = fields.Monetary(
|
||||
readonly=True,
|
||||
help="Asset book value at disposal date.",
|
||||
)
|
||||
gain_loss_amount = fields.Monetary(compute='_compute_gain_loss', store=True)
|
||||
notes = fields.Text()
|
||||
|
||||
move_id = fields.Many2one(
|
||||
'account.move', readonly=True, copy=False,
|
||||
help="Journal entry created for this disposal.",
|
||||
)
|
||||
|
||||
@api.depends('sale_amount', 'book_value_at_disposal', 'disposal_type')
|
||||
def _compute_gain_loss(self):
|
||||
for d in self:
|
||||
if d.disposal_type == 'sale':
|
||||
d.gain_loss_amount = d.sale_amount - d.book_value_at_disposal
|
||||
else:
|
||||
d.gain_loss_amount = -d.book_value_at_disposal
|
||||
@@ -0,0 +1,398 @@
|
||||
"""The asset engine — orchestrator for all asset depreciation + lifecycle.
|
||||
|
||||
7-method public API. No direct ORM writes to fusion.asset.depreciation.line
|
||||
or account.move from anywhere else; everything routes through here for
|
||||
consistent validation, audit, and side-effect handling.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from ..services.depreciation_methods import (
|
||||
straight_line,
|
||||
declining_balance,
|
||||
units_of_production,
|
||||
)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionAssetEngine(models.AbstractModel):
|
||||
_name = "fusion.asset.engine"
|
||||
_description = "Fusion Asset Engine"
|
||||
|
||||
# ============================================================
|
||||
# PUBLIC API (7 methods)
|
||||
# ============================================================
|
||||
|
||||
@api.model
|
||||
def compute_depreciation_schedule(self, asset, *, recompute: bool = False) -> dict:
|
||||
"""Compute (or re-compute) the depreciation board for an asset.
|
||||
|
||||
If recompute=False and posted lines exist, ONLY un-posted future lines
|
||||
are regenerated. If recompute=True, all unposted lines are wiped and
|
||||
regenerated from scratch using current asset config.
|
||||
"""
|
||||
if not asset:
|
||||
raise ValidationError(_("asset is required"))
|
||||
asset.ensure_one()
|
||||
|
||||
self._validate_asset_for_schedule(asset)
|
||||
|
||||
Line = self.env['fusion.asset.depreciation.line'].sudo()
|
||||
if recompute:
|
||||
Line.search([
|
||||
('asset_id', '=', asset.id),
|
||||
('is_posted', '=', False),
|
||||
]).unlink()
|
||||
|
||||
existing_posted = Line.search([
|
||||
('asset_id', '=', asset.id),
|
||||
('is_posted', '=', True),
|
||||
], order='period_index')
|
||||
start_period = max([l.period_index for l in existing_posted], default=-1) + 1
|
||||
accumulated_so_far = sum(l.amount for l in existing_posted)
|
||||
|
||||
steps = self._compute_steps(asset)
|
||||
new_steps = steps[start_period:]
|
||||
|
||||
base_date = asset.in_service_date or asset.acquisition_date
|
||||
|
||||
# Accumulated baseline at the boundary between posted and to-be-created
|
||||
# lines: subtract the accumulated value the algorithm itself reports at
|
||||
# that boundary, then re-add the actually-posted total. This keeps the
|
||||
# board's accumulated column monotonic when picking up mid-life.
|
||||
baseline_offset = 0.0
|
||||
if start_period > 0 and start_period <= len(steps):
|
||||
baseline_offset = steps[start_period - 1].accumulated_depreciation
|
||||
|
||||
line_vals = []
|
||||
for s in new_steps:
|
||||
scheduled_date = self._add_periods(base_date, s.period_index)
|
||||
running_accumulated = round(
|
||||
accumulated_so_far + s.accumulated_depreciation - baseline_offset, 2
|
||||
)
|
||||
line_vals.append({
|
||||
'asset_id': asset.id,
|
||||
'period_index': s.period_index,
|
||||
'scheduled_date': scheduled_date,
|
||||
'amount': s.period_amount,
|
||||
'accumulated': running_accumulated,
|
||||
'book_value_at_end': s.book_value_at_end,
|
||||
'is_posted': False,
|
||||
})
|
||||
if line_vals:
|
||||
Line.create(line_vals)
|
||||
|
||||
return {
|
||||
'asset_id': asset.id,
|
||||
'lines_created': len(line_vals),
|
||||
'total_lines': len(asset.depreciation_line_ids),
|
||||
'method': asset.method,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def post_depreciation_entry(self, asset, *, period_date: date = None) -> dict:
|
||||
"""Post the next-due un-posted depreciation line.
|
||||
|
||||
If period_date provided, post all lines whose scheduled_date <= period_date.
|
||||
Otherwise, post the single next un-posted line (the earliest one).
|
||||
"""
|
||||
asset.ensure_one()
|
||||
if asset.state != 'running':
|
||||
raise ValidationError(
|
||||
_("Cannot post depreciation for asset in state %s") % asset.state
|
||||
)
|
||||
|
||||
Line = self.env['fusion.asset.depreciation.line'].sudo()
|
||||
domain = [('asset_id', '=', asset.id), ('is_posted', '=', False)]
|
||||
if period_date:
|
||||
domain.append(('scheduled_date', '<=', period_date))
|
||||
unposted = Line.search(domain, order='scheduled_date, period_index')
|
||||
if not unposted:
|
||||
return {'posted_count': 0, 'reason': 'no unposted lines due'}
|
||||
|
||||
if not period_date:
|
||||
unposted = unposted[:1]
|
||||
|
||||
posted_ids = []
|
||||
for line in unposted:
|
||||
self._create_journal_entry(asset, line)
|
||||
line.action_post()
|
||||
posted_ids.append(line.id)
|
||||
|
||||
return {'posted_count': len(posted_ids), 'posted_line_ids': posted_ids}
|
||||
|
||||
@api.model
|
||||
def dispose_asset(self, asset, *, sale_amount: float = 0.0,
|
||||
sale_date: date = None, sale_partner=None,
|
||||
disposal_type: str = 'sale') -> dict:
|
||||
"""Dispose an asset (sale, scrap, donation, lost)."""
|
||||
asset.ensure_one()
|
||||
if asset.state == 'disposed':
|
||||
raise ValidationError(_("Asset already disposed."))
|
||||
sale_date = sale_date or fields.Date.today()
|
||||
|
||||
Line = self.env['fusion.asset.depreciation.line'].sudo()
|
||||
future_unposted = Line.search([
|
||||
('asset_id', '=', asset.id),
|
||||
('is_posted', '=', False),
|
||||
('scheduled_date', '>', sale_date),
|
||||
])
|
||||
future_unposted.unlink()
|
||||
|
||||
asset.invalidate_recordset(['book_value', 'total_depreciated'])
|
||||
book_value = asset.book_value
|
||||
|
||||
Disposal = self.env['fusion.asset.disposal'].sudo()
|
||||
partner_id = False
|
||||
if sale_partner:
|
||||
partner_id = sale_partner.id if hasattr(sale_partner, 'id') else sale_partner
|
||||
disposal = Disposal.create({
|
||||
'asset_id': asset.id,
|
||||
'disposal_type': disposal_type,
|
||||
'disposal_date': sale_date,
|
||||
'sale_amount': sale_amount,
|
||||
'sale_partner_id': partner_id,
|
||||
'book_value_at_disposal': book_value,
|
||||
})
|
||||
|
||||
asset.write({
|
||||
'state': 'disposed',
|
||||
'disposed_date': sale_date,
|
||||
})
|
||||
|
||||
return {
|
||||
'asset_id': asset.id,
|
||||
'disposal_id': disposal.id,
|
||||
'gain_loss_amount': disposal.gain_loss_amount,
|
||||
'book_value_at_disposal': book_value,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def partial_sale(self, asset, *, sold_amount: float, sold_qty: float = None,
|
||||
sale_date: date = None, sale_partner=None) -> dict:
|
||||
"""Partially dispose: split asset into two — sold child + remaining parent.
|
||||
|
||||
sold_amount is cash received for the sold portion.
|
||||
sold_qty is the ratio of original cost to attribute to the sold portion (0..1).
|
||||
If sold_qty is None, defaults to sold_amount / cost.
|
||||
"""
|
||||
asset.ensure_one()
|
||||
if asset.state == 'disposed':
|
||||
raise ValidationError(_("Cannot partially sell a disposed asset."))
|
||||
if sold_qty is None:
|
||||
sold_qty = sold_amount / asset.cost if asset.cost else 0
|
||||
if not (0 < sold_qty < 1):
|
||||
raise ValidationError(
|
||||
_("sold_qty must be strictly between 0 and 1; got %s") % sold_qty
|
||||
)
|
||||
|
||||
sale_date = sale_date or fields.Date.today()
|
||||
|
||||
Asset = self.env['fusion.asset'].sudo()
|
||||
sold_cost = round(asset.cost * sold_qty, 2)
|
||||
sold_salvage = round(asset.salvage_value * sold_qty, 2)
|
||||
child_vals = {
|
||||
'name': f"{asset.name} (sold portion)",
|
||||
'parent_id': asset.id,
|
||||
'cost': sold_cost,
|
||||
'salvage_value': sold_salvage,
|
||||
'acquisition_date': asset.acquisition_date,
|
||||
'in_service_date': asset.in_service_date,
|
||||
'method': asset.method,
|
||||
'useful_life_years': asset.useful_life_years,
|
||||
'declining_rate_pct': asset.declining_rate_pct,
|
||||
'prorate_convention': asset.prorate_convention,
|
||||
'company_id': asset.company_id.id,
|
||||
'state': 'running',
|
||||
}
|
||||
if asset.category_id:
|
||||
child_vals['category_id'] = asset.category_id.id
|
||||
child = Asset.create(child_vals)
|
||||
|
||||
new_cost = round(asset.cost - sold_cost, 2)
|
||||
new_salvage = round(asset.salvage_value - sold_salvage, 2)
|
||||
asset.write({
|
||||
'cost': new_cost,
|
||||
'salvage_value': new_salvage,
|
||||
})
|
||||
self.compute_depreciation_schedule(asset, recompute=True)
|
||||
|
||||
result = self.dispose_asset(
|
||||
child, sale_amount=sold_amount, sale_date=sale_date,
|
||||
sale_partner=sale_partner, disposal_type='sale',
|
||||
)
|
||||
return {
|
||||
'parent_asset_id': asset.id,
|
||||
'child_asset_id': child.id,
|
||||
'disposal_id': result['disposal_id'],
|
||||
'gain_loss_amount': result['gain_loss_amount'],
|
||||
}
|
||||
|
||||
@api.model
|
||||
def pause_asset(self, asset, pause_date: date = None) -> dict:
|
||||
"""Pause depreciation. Wraps asset.action_pause for API symmetry and
|
||||
to log the pause date for downstream auditing."""
|
||||
asset.ensure_one()
|
||||
asset.action_pause()
|
||||
return {
|
||||
'asset_id': asset.id,
|
||||
'pause_date': pause_date or fields.Date.today(),
|
||||
'state': 'paused',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def resume_asset(self, asset, resume_date: date = None) -> dict:
|
||||
"""Resume a paused asset."""
|
||||
asset.ensure_one()
|
||||
asset.action_resume()
|
||||
return {
|
||||
'asset_id': asset.id,
|
||||
'resume_date': resume_date or fields.Date.today(),
|
||||
'state': 'running',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def reverse_disposal(self, asset) -> dict:
|
||||
"""Reverse a disposal (rare — recovery from accidental sale entry)."""
|
||||
asset.ensure_one()
|
||||
if asset.state != 'disposed':
|
||||
raise ValidationError(_("Asset is not disposed."))
|
||||
|
||||
Disposal = self.env['fusion.asset.disposal'].sudo()
|
||||
last_disposal = Disposal.search(
|
||||
[('asset_id', '=', asset.id)],
|
||||
order='disposal_date desc, id desc', limit=1,
|
||||
)
|
||||
if last_disposal and last_disposal.move_id:
|
||||
try:
|
||||
last_disposal.move_id.button_cancel()
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning("Could not cancel disposal move: %s", e)
|
||||
if last_disposal:
|
||||
last_disposal.unlink()
|
||||
asset.write({'state': 'running', 'disposed_date': False})
|
||||
return {'asset_id': asset.id, 'state': 'running'}
|
||||
|
||||
# ============================================================
|
||||
# PRIVATE HELPERS
|
||||
# ============================================================
|
||||
|
||||
def _validate_asset_for_schedule(self, asset):
|
||||
if asset.cost <= 0:
|
||||
raise ValidationError(_("Asset cost must be > 0 to compute schedule."))
|
||||
if asset.method == 'units_of_production' and not asset.total_units_expected:
|
||||
raise ValidationError(_(
|
||||
"Units of Production assets need total_units_expected set."
|
||||
))
|
||||
if asset.method in ('straight_line', 'declining_balance'):
|
||||
if asset.useful_life_years < 1:
|
||||
raise ValidationError(_("useful_life_years must be >= 1."))
|
||||
if asset.salvage_value > asset.cost:
|
||||
raise ValidationError(_("Salvage value cannot exceed cost."))
|
||||
|
||||
def _compute_steps(self, asset) -> list:
|
||||
"""Dispatch to the appropriate depreciation method service."""
|
||||
if asset.method == 'straight_line':
|
||||
return straight_line(
|
||||
cost=asset.cost,
|
||||
salvage_value=asset.salvage_value,
|
||||
n_periods=asset.useful_life_years,
|
||||
)
|
||||
if asset.method == 'declining_balance':
|
||||
return declining_balance(
|
||||
cost=asset.cost,
|
||||
salvage_value=asset.salvage_value,
|
||||
n_periods=asset.useful_life_years,
|
||||
rate=asset.declining_rate_pct / 100.0,
|
||||
)
|
||||
if asset.method == 'units_of_production':
|
||||
# Phase 3 simple: assume even per-period units. Phase 3.5 can read
|
||||
# from a per-period usage table populated by maintenance/IoT data.
|
||||
if asset.useful_life_years:
|
||||
per_period = asset.total_units_expected / asset.useful_life_years
|
||||
periods = asset.useful_life_years
|
||||
else:
|
||||
per_period = asset.total_units_expected
|
||||
periods = 1
|
||||
return units_of_production(
|
||||
cost=asset.cost,
|
||||
salvage_value=asset.salvage_value,
|
||||
total_units_expected=asset.total_units_expected,
|
||||
units_per_period=[per_period] * periods,
|
||||
)
|
||||
return []
|
||||
|
||||
def _add_periods(self, base_date: date, n_periods: int) -> date:
|
||||
"""Add (n_periods + 1) yearly increments to base_date and step back one
|
||||
day, giving the period-end date.
|
||||
|
||||
Phase 3.5 can split this into monthly/quarterly variants when the asset
|
||||
carries a sub-annual frequency.
|
||||
"""
|
||||
try:
|
||||
return base_date.replace(year=base_date.year + n_periods + 1) - timedelta(days=1)
|
||||
except ValueError:
|
||||
return base_date.replace(
|
||||
year=base_date.year + n_periods + 1, day=28,
|
||||
) - timedelta(days=1)
|
||||
|
||||
def _create_journal_entry(self, asset, line):
|
||||
"""Create the journal entry for a depreciation line.
|
||||
|
||||
Phase 3 keeps this minimal: requires the category to have both
|
||||
depreciation_account_id and expense_account_id wired up. Without that,
|
||||
the line is still posted (is_posted flag) but no move is created.
|
||||
Phase 3.5 will add multi-currency, allocation rules, and analytic tags.
|
||||
"""
|
||||
category = asset.category_id
|
||||
if not category or not (category.depreciation_account_id and category.expense_account_id):
|
||||
_logger.debug(
|
||||
"No accounts on category for asset %s; skipping journal entry",
|
||||
asset.id,
|
||||
)
|
||||
return None
|
||||
Move = self.env['account.move'].sudo()
|
||||
journal = self.env['account.journal'].search([
|
||||
('type', '=', 'general'),
|
||||
('company_id', '=', asset.company_id.id),
|
||||
], limit=1)
|
||||
if not journal:
|
||||
_logger.warning(
|
||||
"No general journal for company %s; skipping move creation",
|
||||
asset.company_id.name,
|
||||
)
|
||||
return None
|
||||
try:
|
||||
move = Move.create({
|
||||
'date': line.scheduled_date,
|
||||
'journal_id': journal.id,
|
||||
'ref': f"Depreciation: {asset.name} (P{line.period_index + 1})",
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': f"Depreciation expense - {asset.name}",
|
||||
'account_id': category.expense_account_id.id,
|
||||
'debit': line.amount,
|
||||
'credit': 0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': f"Accumulated depreciation - {asset.name}",
|
||||
'account_id': category.depreciation_account_id.id,
|
||||
'debit': 0,
|
||||
'credit': line.amount,
|
||||
}),
|
||||
],
|
||||
})
|
||||
move.action_post()
|
||||
line.write({'move_id': move.id})
|
||||
return move
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning(
|
||||
"Failed to create depreciation move for asset %s line %s: %s",
|
||||
asset.id, line.id, e,
|
||||
)
|
||||
return None
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Cron handlers for fusion_accounting_assets.
|
||||
|
||||
- _cron_post_due_depreciation: daily, post due depreciation lines for running assets
|
||||
- _cron_anomaly_scan: monthly, scan for schedule variance and create anomaly records
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from ..services.anomaly_detection import detect_schedule_variance
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionAssetsCron(models.AbstractModel):
|
||||
_name = "fusion.assets.cron"
|
||||
_description = "Fusion Assets Cron Handlers"
|
||||
|
||||
@api.model
|
||||
def _cron_post_due_depreciation(self):
|
||||
"""For each running asset, post any due un-posted depreciation lines."""
|
||||
today = fields.Date.today()
|
||||
engine = self.env['fusion.asset.engine']
|
||||
Asset = self.env['fusion.asset']
|
||||
running_assets = Asset.search([('state', '=', 'running')])
|
||||
posted_total = 0
|
||||
for asset in running_assets:
|
||||
try:
|
||||
with self.env.cr.savepoint():
|
||||
result = engine.post_depreciation_entry(asset, period_date=today)
|
||||
posted_total += result.get('posted_count', 0)
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning("Cron post failed for asset %s: %s", asset.id, e)
|
||||
_logger.info(
|
||||
"Cron: posted depreciation on %d lines across %d running assets",
|
||||
posted_total, len(running_assets),
|
||||
)
|
||||
# Keep the book-value MV in sync after posting so the dashboard
|
||||
# reflects today's numbers without waiting for the dedicated MV cron.
|
||||
try:
|
||||
self.env['fusion.asset.book.values.mv']._refresh()
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning("Post-cron MV refresh failed: %s", e)
|
||||
|
||||
@api.model
|
||||
def _cron_refresh_book_values_mv(self):
|
||||
"""Refresh the per-asset book value MV (hourly)."""
|
||||
self.env['fusion.asset.book.values.mv']._refresh()
|
||||
|
||||
@api.model
|
||||
def _cron_anomaly_scan(self):
|
||||
"""For each running asset, compare expected accumulated depreciation
|
||||
vs posted, and persist any variance flags."""
|
||||
Asset = self.env['fusion.asset']
|
||||
Anomaly = self.env['fusion.asset.anomaly']
|
||||
running_assets = Asset.search([('state', '=', 'running')])
|
||||
flagged = 0
|
||||
today = fields.Date.today()
|
||||
for asset in running_assets:
|
||||
try:
|
||||
expected = sum(
|
||||
l.amount for l in asset.depreciation_line_ids
|
||||
if l.scheduled_date and l.scheduled_date <= today
|
||||
)
|
||||
actual = asset.total_depreciated
|
||||
anomaly = detect_schedule_variance(
|
||||
asset_id=asset.id, asset_name=asset.name,
|
||||
expected_accumulated=expected, actual_accumulated=actual,
|
||||
)
|
||||
if anomaly is None:
|
||||
continue
|
||||
anomaly_dict = anomaly.to_dict()
|
||||
existing = Anomaly.search([
|
||||
('asset_id', '=', asset.id),
|
||||
('anomaly_type', '=', anomaly_dict['anomaly_type']),
|
||||
('state', 'in', ('new', 'acknowledged')),
|
||||
], limit=1)
|
||||
if existing:
|
||||
continue
|
||||
Anomaly.create({
|
||||
'asset_id': asset.id,
|
||||
'anomaly_type': anomaly_dict['anomaly_type'],
|
||||
'severity': anomaly_dict['severity'],
|
||||
'expected': anomaly_dict['expected'],
|
||||
'actual': anomaly_dict['actual'],
|
||||
'variance_pct': anomaly_dict['variance_pct'],
|
||||
'detail': anomaly_dict['detail'],
|
||||
})
|
||||
flagged += 1
|
||||
except Exception as e: # noqa: BLE001
|
||||
_logger.warning("Cron anomaly scan failed for asset %s: %s", asset.id, e)
|
||||
_logger.info(
|
||||
"Cron: scanned %d assets, flagged %d anomalies",
|
||||
len(running_assets), flagged,
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Assets-specific migration step.
|
||||
|
||||
Backfills fusion.asset from existing account.asset rows (Enterprise) so users
|
||||
get all their existing assets in the Fusion namespace after switchover."""
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Map Enterprise method names to Fusion method names
|
||||
ENTERPRISE_METHOD_MAP = {
|
||||
'linear': 'straight_line',
|
||||
'degressive': 'declining_balance',
|
||||
'degressive_then_linear': 'declining_balance', # simplified
|
||||
'manual': 'straight_line',
|
||||
'unit_of_production': 'units_of_production',
|
||||
'units_of_production': 'units_of_production',
|
||||
}
|
||||
|
||||
|
||||
class FusionMigrationWizard(models.TransientModel):
|
||||
_inherit = "fusion.migration.wizard"
|
||||
|
||||
def _assets_bootstrap_step(self):
|
||||
"""Backfill fusion.asset from account.asset (Enterprise) if it exists."""
|
||||
result = {
|
||||
'step': 'assets_bootstrap',
|
||||
'enterprise_module_present': False,
|
||||
'created': 0, 'skipped': 0, 'errors': [],
|
||||
}
|
||||
# Check if Enterprise account.asset exists
|
||||
AccountAsset = self.env.get('account.asset')
|
||||
if AccountAsset is None:
|
||||
result['enterprise_module_present'] = False
|
||||
return result
|
||||
result['enterprise_module_present'] = True
|
||||
|
||||
FusionAsset = self.env['fusion.asset'].sudo()
|
||||
|
||||
# Iterate Enterprise records
|
||||
company_id = self.company_id.id if 'company_id' in self._fields and self.company_id else None
|
||||
domain = []
|
||||
if company_id:
|
||||
domain.append(('company_id', '=', company_id))
|
||||
|
||||
try:
|
||||
ea_records = AccountAsset.sudo().search(domain, limit=10000)
|
||||
except Exception as e:
|
||||
result['errors'].append(f"Enterprise search failed: {e}")
|
||||
return result
|
||||
|
||||
for ea in ea_records:
|
||||
try:
|
||||
# Idempotent: skip if a fusion asset with same source name exists
|
||||
existing = FusionAsset.search([
|
||||
('name', '=', ea.name),
|
||||
('cost', '=', getattr(ea, 'original_value', 0) or 0),
|
||||
('company_id', '=', ea.company_id.id),
|
||||
], limit=1)
|
||||
if existing:
|
||||
result['skipped'] += 1
|
||||
continue
|
||||
|
||||
# Map state — Enterprise has 'draft', 'open' (running), 'paused', 'close' (disposed)
|
||||
ea_state = getattr(ea, 'state', 'draft')
|
||||
state_map = {'draft': 'draft', 'open': 'running',
|
||||
'paused': 'paused', 'close': 'disposed',
|
||||
'model': 'draft'}
|
||||
state = state_map.get(ea_state, 'draft')
|
||||
|
||||
method = ENTERPRISE_METHOD_MAP.get(
|
||||
getattr(ea, 'method', 'linear'), 'straight_line')
|
||||
|
||||
FusionAsset.create({
|
||||
'name': ea.name,
|
||||
'cost': getattr(ea, 'original_value', 0) or 0,
|
||||
'salvage_value': getattr(ea, 'salvage_value', 0) or 0,
|
||||
'acquisition_date': getattr(ea, 'acquisition_date', False) or fields.Date.today(),
|
||||
'in_service_date': getattr(ea, 'prorata_date', False) or False,
|
||||
'method': method,
|
||||
'useful_life_years': getattr(ea, 'method_number', 5) or 5,
|
||||
'declining_rate_pct': getattr(ea, 'method_progress_factor', 0.2) * 100 if hasattr(ea, 'method_progress_factor') else 20.0,
|
||||
'company_id': ea.company_id.id,
|
||||
'state': state,
|
||||
})
|
||||
result['created'] += 1
|
||||
except Exception as e:
|
||||
result['errors'].append(f"Asset {ea.id}: {e}")
|
||||
|
||||
_logger.info(
|
||||
"fusion_accounting_assets migration: %d created, %d skipped, %d errors",
|
||||
result['created'], result['skipped'], len(result['errors']))
|
||||
return result
|
||||
|
||||
def action_run_migration(self):
|
||||
"""Override to add assets-bootstrap step."""
|
||||
result = super().action_run_migration() if hasattr(super(), 'action_run_migration') else None
|
||||
try:
|
||||
self._assets_bootstrap_step()
|
||||
except Exception as e:
|
||||
_logger.warning("assets_bootstrap_step failed: %s", e)
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
from . import migration_audit_report
|
||||
@@ -0,0 +1,36 @@
|
||||
"""QWeb PDF: migration audit report for fusion_accounting_assets."""
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class FusionAssetsMigrationAuditReport(models.AbstractModel):
|
||||
_name = "report.fusion_accounting_assets.migration_audit_template"
|
||||
_description = "Fusion Assets Migration Audit"
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
wizards = self.env['fusion.migration.wizard'].browse(docids) if docids else self.env['fusion.migration.wizard']
|
||||
Asset = self.env['fusion.asset']
|
||||
company_stats = []
|
||||
for company in self.env['res.company'].search([]):
|
||||
assets = Asset.search([('company_id', '=', company.id)])
|
||||
by_state = {}
|
||||
for state in ('draft', 'running', 'paused', 'disposed'):
|
||||
by_state[state] = sum(1 for a in assets if a.state == state)
|
||||
total_cost = sum(a.cost for a in assets)
|
||||
total_book = sum(a.book_value for a in assets)
|
||||
total_dep = sum(a.total_depreciated for a in assets)
|
||||
company_stats.append({
|
||||
'company': company,
|
||||
'count': len(assets),
|
||||
'by_state': by_state,
|
||||
'total_cost': total_cost,
|
||||
'total_book_value': total_book,
|
||||
'total_depreciated': total_dep,
|
||||
})
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'fusion.migration.wizard',
|
||||
'docs': wizards,
|
||||
'company_stats': company_stats,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="action_report_assets_migration_audit" model="ir.actions.report">
|
||||
<field name="name">Assets Migration Audit</field>
|
||||
<field name="model">fusion.migration.wizard</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_accounting_assets.migration_audit_template</field>
|
||||
<field name="report_file">fusion_accounting_assets.migration_audit_template</field>
|
||||
<field name="binding_model_id" ref="fusion_accounting_migration.model_fusion_migration_wizard"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="migration_audit_template">
|
||||
<t t-call="web.html_container">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>Fusion Assets Migration Audit</h2>
|
||||
<p>
|
||||
<span t-esc="context_timestamp(datetime.datetime.now()).strftime('%Y-%m-%d %H:%M')"/>
|
||||
</p>
|
||||
|
||||
<h3>Per-Company Summary</h3>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th class="text-end">Total Assets</th>
|
||||
<th class="text-end">Draft</th>
|
||||
<th class="text-end">Running</th>
|
||||
<th class="text-end">Paused</th>
|
||||
<th class="text-end">Disposed</th>
|
||||
<th class="text-end">Total Cost</th>
|
||||
<th class="text-end">Total NBV</th>
|
||||
<th class="text-end">Total Depreciated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="company_stats" t-as="cs">
|
||||
<td><span t-field="cs['company'].name"/></td>
|
||||
<td class="text-end"><span t-esc="cs['count']"/></td>
|
||||
<td class="text-end"><span t-esc="cs['by_state']['draft']"/></td>
|
||||
<td class="text-end"><span t-esc="cs['by_state']['running']"/></td>
|
||||
<td class="text-end"><span t-esc="cs['by_state']['paused']"/></td>
|
||||
<td class="text-end"><span t-esc="cs['by_state']['disposed']"/></td>
|
||||
<td class="text-end"><span t-esc="'{:,.2f}'.format(cs['total_cost'])"/></td>
|
||||
<td class="text-end"><span t-esc="'{:,.2f}'.format(cs['total_book_value'])"/></td>
|
||||
<td class="text-end"><span t-esc="'{:,.2f}'.format(cs['total_depreciated'])"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="text-muted small">
|
||||
Generated by Fusion Accounting Assets
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,15 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fusion_asset_user,fusion.asset.user,model_fusion_asset,base.group_user,1,0,0,0
|
||||
access_fusion_asset_admin,fusion.asset.admin,model_fusion_asset,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_asset_depreciation_line_user,fusion.asset.depreciation.line.user,model_fusion_asset_depreciation_line,base.group_user,1,0,0,0
|
||||
access_fusion_asset_depreciation_line_admin,fusion.asset.depreciation.line.admin,model_fusion_asset_depreciation_line,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_asset_category_user,fusion.asset.category.user,model_fusion_asset_category,base.group_user,1,0,0,0
|
||||
access_fusion_asset_category_admin,fusion.asset.category.admin,model_fusion_asset_category,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_asset_disposal_user,fusion.asset.disposal.user,model_fusion_asset_disposal,base.group_user,1,0,0,0
|
||||
access_fusion_asset_disposal_admin,fusion.asset.disposal.admin,model_fusion_asset_disposal,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_asset_anomaly_user,fusion.asset.anomaly.user,model_fusion_asset_anomaly,base.group_user,1,0,0,0
|
||||
access_fusion_asset_anomaly_admin,fusion.asset.anomaly.admin,model_fusion_asset_anomaly,fusion_accounting_core.group_fusion_accounting_admin,1,1,1,1
|
||||
access_fusion_create_asset_wizard_user,fusion.create.asset.wizard.user,model_fusion_create_asset_wizard,base.group_user,1,1,1,0
|
||||
access_fusion_disposal_wizard_user,fusion.disposal.wizard.user,model_fusion_disposal_wizard,base.group_user,1,1,1,0
|
||||
access_fusion_partial_sale_wizard_user,fusion.partial.sale.wizard.user,model_fusion_partial_sale_wizard,base.group_user,1,1,1,0
|
||||
access_fusion_depreciation_run_wizard_user,fusion.depreciation.run.wizard.user,model_fusion_depreciation_run_wizard,base.group_user,1,1,1,0
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user