Compare commits

..

20 Commits

Author SHA1 Message Date
f3766c2898 feat: add x_fc_authorizer_number, x_fc_account_number, x_marked_for fields; auto-link authorizer from XML
- fusion_claims: added x_fc_authorizer_number to res.partner for ADP authorizer registration numbers
- fusion_claims: XML parser auto-links authorizer contact to sale order by ADP number
- fusion_claims: removed size=9 constraint from x_fc_odsp_member_id
- fusion_claims: authorizer number shown on OT/PT contact form
- fusion_so_to_po: added x_marked_for (Many2one) field definition on purchase.order
- fusion_so_to_po: added x_fc_account_number on res.partner for vendor account numbers
2026-03-11 17:22:02 +00:00
431052920e feat: separate fusion field service and LTC into standalone modules, update core modules
- fusion_claims: separated field service logic, updated controllers/views
- fusion_tasks: updated task views and map integration
- fusion_authorizer_portal: added page 11 signing, schedule booking, migrations
- fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator)
- fusion_ltc_management: new standalone LTC management module
2026-03-11 16:19:52 +00:00
1f79cdcaaf fix: improve AI chat table rendering with CSS styling and narrow-panel formatting
- Add SCSS for AI chat tables: borders, padding, zebra striping, hover, dark mode
- Style headings, code, bold text, and lists in chat messages
- Update system prompt: enforce 3-column max tables for narrow chat panel
- Use key-value (2-column) tables for summaries, split wide data into sections
- Provide explicit correct/wrong format examples in prompt
2026-03-10 02:54:04 +00:00
8761d0e7c7 feat: add Demographics & Analytics tool to Fusion Claims Intelligence
- Add Tool 6 for demographic analysis using direct SQL queries
- Age group breakdowns: clients, applications, avg apps/client, avg funding
- Device popularity by age bracket (under 45, 45-60, 61-75, 75+)
- City demographics with average age and funding per city
- Benefit type analysis (ODSP, OWP, ACSD, Regular)
- Top devices with average client age
- Overall funding summary (totals, averages, age range)
- Update AI topic and system prompt with Tool 6 routing examples
2026-03-10 02:45:51 +00:00
0053576cc2 fix: enable rich text markdown formatting for AI agent responses
- Install markdown2 dependency for Odoo AI module
- Update system prompt with explicit markdown formatting instructions
- Add example templates for client status and billing period responses
- Use tables, bold, headings, and code formatting for clean output
2026-03-10 02:39:02 +00:00
7bd7b8f7c4 fix: enhance Fusion Claims Intelligence AI with client status and billing period tools
- Fix _read_group override crash (dict_values not subscriptable) in sale_order.py
- Migrate _fc_tool_claims_stats from deprecated read_group() to _read_group() API
- Enrich client details tool with funding history, invoice status, prev-funded devices
- Add Client Status Lookup tool (search by name, returns orders/invoices/next steps)
- Add ADP Billing Period tool (invoiced amounts, paid/unpaid, submission deadlines)
- Update AI agent system prompt with all 5 tools and usage examples
2026-03-10 02:30:42 +00:00
3342b57469 feat: reorder search views - Customer first, add delivery/tags/status fields for ADP, ODSP, MOD 2026-03-10 01:46:15 +00:00
1bfa50aa5f feat: View Details uses ADP landscape report for ADP orders, add route decorators 2026-03-09 22:55:53 +00:00
85367747a6 fix: remove _get_display_grouped_section() call causing 500 error on portal 2026-03-09 22:49:55 +00:00
d7657bb356 feat: add borders, ADP Device Code, ADP/Client Portion columns and subtotals to portal view 2026-03-09 22:46:16 +00:00
9dac39853f fix: revert POD signature to original layout - only quotation reports need organized signature 2026-03-09 22:32:27 +00:00
c1a3b02ac5 fix: improve ADP report signature section with legal terms, date/time, printed name; switch portal to landscape report 2026-03-09 22:03:46 +00:00
1f750a6db4 fix: improve ADP report signature section with legal terms, date/time, printed name; switch portal to landscape report 2026-03-09 22:03:28 +00:00
ffcc83d7bd fix: improve ADP report signature section with legal terms, date/time, printed name; switch portal to landscape report 2026-03-09 22:03:12 +00:00
6c3c565440 fix: improve ADP report signature section with legal terms, date/time, printed name; switch portal to landscape report 2026-03-09 22:02:53 +00:00
1c191a54e1 fix: ADP portal sign/pay modal text - show client portion, not full total 2026-03-09 21:34:55 +00:00
512aedce69 fix: ADP portal - sidebar amount, claim details, signature report 2026-03-09 21:25:37 +00:00
f362fbd915 fix: ADP portal - sidebar amount, claim details, signature report 2026-03-09 21:25:05 +00:00
Nexa Agent
35399170b3 fix: ADP portal payment uses client portion instead of full order total
When customers pay for ADP quotations through the portal, the system
was charging the full order amount (ADP + client portions combined).
Now correctly charges only the client portion (25% for REG clients).

Changes:
- Override _get_prepayment_required_amount() to return client portion
- Override _has_to_be_paid() to skip payment for 100% ADP-funded orders
- Add portal controller to cap payment amount at client portion
- Add portal template showing ADP funding breakdown to customer
2026-03-09 21:11:19 +00:00
gsinghpal
3b3c57205a feat: add fusion_tasks module for field service management
Standalone module extracted from fusion_claims providing technician
scheduling, route simulation with Google Maps, GPS tracking, and
cross-instance task sync between odoo-westin and odoo-mobility.

Includes fix for route simulation: added Directions API error logging
to diagnose silent failures from conflicting Google Maps API keys.

Made-with: Cursor
2026-03-09 16:56:53 -04:00
5645 changed files with 199696 additions and 589920 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,12 +0,0 @@
{
"permissions": {
"allow": [
"WebFetch(domain:docs.clover.com)"
]
},
"hooks": {
"UserPromptSubmit": [],
"Stop": [],
"Notification": []
}
}

View File

@@ -1,44 +0,0 @@
---
description: Identify and verify target environment (production vs local dev) before ANY state-changing operation. Never assume; always verify.
alwaysApply: true
---
# Environment Safety — Production vs Local Dev
**The ssh alias `odoo-westin` (192.168.1.40, erp.westinhealthcare.ca) is PRODUCTION.** Do NOT test against it. `docker exec odoo-dev-app ...` via this ssh alias touches PRODUCTION despite the "-dev" in the container name.
**Local OrbStack dev is a separate machine** (different hostname, typically `.orb.local` domain, accessed via a different connection path). Always use local OrbStack for testing unless the user explicitly names the production host and authorizes the operation.
## Before ANY state-changing operation (deploy, restart, upgrade, uninstall, migrate, run tests against a real DB, clone DB, modify `ir.config_parameter`), you MUST:
1. **Read the `odoo.conf` header.** If it contains `PRODUCTION`, stop and confirm with user.
2. **Check the SSH target.** If the host/alias resolves to a public-facing domain (`erp.*`, customer-facing URL) or a LAN IP outside `127.0.0.0/8` and the user hasn't authorized production, stop.
3. **Check the DB name + data scale.** Databases with tens of thousands of `account.move` rows or real client names in `res.company` are production regardless of what the container is called.
4. **Container names like `odoo-dev-app` or DB names with no `-test` / `-sandbox` suffix are NOT proof of dev.** Ignore naming hints.
## Ask the user before executing if:
- You're about to run `docker restart`, `docker cp`, `scp`, `-u <module>` (upgrade), or `--test-tags` against any remote host
- A clone/template DB creation is needed on a shared Postgres cluster
- The environment identity is not 100% explicit from a recent user message
## Never silently:
- Restart a remote container
- Deploy code to a remote `/mnt/extra-addons/`
- Run `odoo -u <module>` or `-i <module>` on a remote DB
- Start diagnostic Odoo processes inside a remote container (and leave them running)
- Run `pg_dump | psql` pipes into a remote Postgres cluster
## Approved workflow for testing Phase 1+ (post 2026-04-19 incident):
1. ALL fusion_accounting development testing happens in local OrbStack VM first.
2. Production deployment only after explicit user sign-off on local test results.
3. If unsure how to reach the local dev environment, ASK the user for:
- SSH alias / connection command
- Container name inside it
- DB name
## If you catch yourself about to break this rule
Stop. Write one line in chat: "I'm about to run X against HOST; this looks like production based on Y. Proceed?" Wait for explicit confirmation.

View File

@@ -1,135 +0,0 @@
---
description: Use the Fusion API module for all API calls (OpenAI, Anthropic, Google Maps, Twilio, OAuth) in any Fusion module
globs: fusion_*/models/**/*.py, fusion_*/services/**/*.py
alwaysApply: false
---
# Fusion API Integration Guide
When any Fusion module needs to call an external API (OpenAI, Anthropic, Google Maps, Twilio, Google/Microsoft OAuth), it MUST use the centralized `fusion.api.service` instead of managing its own keys.
The service lives at `fusion_api/models/api_service.py` and is an AbstractModel accessible via `self.env['fusion.api.service']`.
## Available Methods
### 1. `call_openai()` -- AI text generation via OpenAI
```python
result = self.env['fusion.api.service'].call_openai(
consumer='fusion_clock_ai', # your module's technical name
feature='timesheet_summary', # descriptive feature name for tracking
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': 'Summarize this timesheet...'},
],
model='gpt-4o-mini', # optional, defaults to gpt-4o-mini
max_tokens=1024, # optional, defaults to 1024
user=self.env.user, # optional, defaults to current user
)
# Returns: str (the text response)
```
### 2. `call_anthropic()` -- AI text generation via Claude
```python
result = self.env['fusion.api.service'].call_anthropic(
consumer='fusion_notes',
feature='voice_transcription',
messages=[
{'role': 'user', 'content': 'Transcribe this text...'},
],
system='You are a medical transcription assistant.', # optional system prompt
model='claude-sonnet-4-20250514', # optional, defaults to claude-sonnet-4-20250514
max_tokens=1024, # optional
)
# Returns: str (the text response)
```
### 3. `get_api_key()` -- raw API key for non-AI services
```python
api_key = self.env['fusion.api.service'].get_api_key(
provider_type='google_maps', # one of: google_maps, twilio, custom
consumer='fusion_shipping', # your module's technical name
feature='geocoding', # optional feature name
)
# Returns: str (the raw API key to use in your own HTTP calls)
```
### 4. `get_oauth_credentials()` -- OAuth tokens for Google/Microsoft
```python
creds = self.env['fusion.api.service'].get_oauth_credentials(
provider_type='google_oauth', # google_oauth or microsoft_oauth
consumer='fusion_schedule',
feature='calendar_sync',
)
# Returns: dict with keys:
# client_id, client_secret, access_token, refresh_token, token_expiry, redirect_uri
```
## Provider Types
| Type | Use For |
|------|---------|
| `openai` | GPT-4o, GPT-4o-mini, o1, etc. |
| `anthropic` | Claude Sonnet, Haiku, Opus |
| `google_maps` | Geocoding, Places, Distance Matrix |
| `google_oauth` | Google Calendar, Drive OAuth |
| `microsoft_oauth` | Microsoft Calendar, Graph OAuth |
| `twilio` | SMS, Voice |
| `custom` | Any other API provider |
## What Happens Automatically
- The calling module is auto-registered as a consumer in Fusion API
- Every call is logged with: tokens, cost, response time, user, feature, model
- Budget caps (monthly/daily) are enforced before each call
- Rate limits (RPM/RPD) are enforced before each call
- Per-user limits are checked if configured
- On limit exceeded, a `UserError` is raised with a clear message
## Phased Migration Pattern
Do NOT add `fusion_api` as a hard dependency. Use a try/fallback pattern so the module works with or without Fusion API installed:
```python
def _call_ai(self, messages, feature='general'):
"""Call OpenAI through Fusion API, falling back to own key."""
try:
return self.env['fusion.api.service'].call_openai(
consumer='fusion_clock_ai',
feature=feature,
messages=messages,
)
except Exception:
api_key = self.env['ir.config_parameter'].sudo().get_param(
'fusion_clock_ai.openai_api_key'
)
if not api_key:
raise
from openai import OpenAI
client = OpenAI(api_key=api_key)
response = client.chat.completions.create(
model='gpt-4o-mini', messages=messages,
)
return response.choices[0].message.content
```
## Rules
- NEVER hardcode API keys in Python code
- NEVER store API keys in `ir.config_parameter` for new modules -- use Fusion API
- ALWAYS pass the module's technical name (e.g. `fusion_clock_ai`) as the `consumer` parameter
- ALWAYS pass a descriptive `feature` string for usage tracking granularity
- NEVER catch and silently swallow errors from `fusion.api.service` -- let UserError propagate
- DO NOT add `fusion_api` to `depends` in `__manifest__.py` -- use the fallback pattern above
- The `consumer` string should match the module's technical name exactly (folder name)
## Odoo 19 Conventions
- This project targets Odoo 19 (Community/Enterprise)
- No hardcoded colors in views or SCSS -- let Odoo handle dark/light theming
- Use `res.groups.privilege` for security groups (not `category_id`)
- Do not use deprecated `ir.cron` fields (`numbercall`, `doall`)
- Settings views: use `name=` attribute on `<app>` tags, not `data-key=`

View File

@@ -1,79 +0,0 @@
name: fusion_accounting CI
on:
push:
paths:
- 'fusion_accounting/**'
- 'fusion_accounting_core/**'
- 'fusion_accounting_ai/**'
- 'fusion_accounting_migration/**'
- '.gitea/workflows/fusion_accounting_ci.yml'
pull_request:
paths:
- 'fusion_accounting/**'
- 'fusion_accounting_core/**'
- 'fusion_accounting_ai/**'
- 'fusion_accounting_migration/**'
jobs:
test:
# NOTE: This workflow assumes a self-hosted runner (or Docker-in-Docker)
# that provides an Odoo 19 install. Adjust the `runs-on` and
# `Install Odoo 19` step to match Nexa's environment.
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo
POSTGRES_DB: postgres
ports: ['5432:5432']
options: --health-cmd pg_isready --health-interval 10s
strategy:
fail-fast: false
matrix:
sub_module:
- fusion_accounting_core
- fusion_accounting_ai
- fusion_accounting_migration
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install AI client deps
run: |
pip install --break-system-packages anthropic openai
- name: Install Odoo 19
run: |
# TODO(Phase 1 CI hardening): align with Nexa's Odoo 19 source-of-truth.
# Option A: pull the same image used at odoo-westin (docker pull <registry>/odoo:19)
# Option B: odoo-bin pip install from the pinned Odoo 19 tag
# Option C: host a self-hosted runner on odoo-westin with Odoo pre-installed
echo "TODO: install Odoo 19 here"
exit 1 # fail loudly until this step is implemented
- name: Stage fusion sub-modules in addons-path
run: |
mkdir -p /tmp/addons
cp -r fusion_accounting fusion_accounting_core fusion_accounting_ai fusion_accounting_migration /tmp/addons/
- name: Install + Test ${{ matrix.sub_module }}
run: |
createdb -h localhost -U odoo fusion_test_${{ matrix.sub_module }}
odoo --addons-path=/tmp/addons \
-d fusion_test_${{ matrix.sub_module }} \
-i ${{ matrix.sub_module }} \
--test-tags post_install \
--stop-after-init \
--without-demo=all \
--log-handler=odoo.tests:INFO
env:
PGPASSWORD: odoo

72
.gitignore vendored
View File

@@ -1,70 +1,2 @@
# Python bytecode **/__pycache__/
__pycache__/ *.pyc
*.py[cod]
*$py.class
# Editor / OS noise
.DS_Store
*.swp
*.swo
.vscode/
.idea/
# Odoo runtime
*.pyc-tmp
# Local-only diagnostic logs from test runs
_test_*.log
# --- Split-out module repos (now independent git repos; managed separately) ---
/disable_iap_calls/
/disable_odoo_online/
/disable_publisher_warranty/
/fusion_accounts/
/fusion_api/
/fusion_canada_post/
/fusion_centralize_billing/
/fusion_chatter_enhance/
/fusion_claims/
/fusion_clock/
/fusion_clock_ai/
/fusion_clover/
/fusion_digitize/
/fusion_faxes/
/fusion_helpdesk/
/fusion_helpdesk_central/
/fusion_inventory/
/fusion_loaners_management/
/fusion_login_audit/
/fusion_ltc_management/
/fusion_notes/
/fusion_odoo_fixes/
/fusion_payroll/
/fusion_pdf_preview/
/fusion_planning/
/fusion_portal/
/fusion_poynt/
/fusion_rental/
/fusion_repairs/
/fusion_reports_templates/
/fusion_ringcentral/
/fusion_schedule/
/fusion_service_charges/
/fusion_shipping/
/fusion_so_to_po/
/fusion_tasks/
/fusion_templates/
/fusion_theme_switcher/
/fusion_voip_ringcentral/
/fusion_whitelabels/
/network_logger/
/nexa_coa_setup/
/fusion_plating/
/fusion_accounting/
/fusion_iot/
/fusion_labels/
/fusion_projects/
/fusion-statements/
/fusion-woo-odoo/
/fusion-expenses/
/fusion_configurator/

View File

@@ -1,6 +0,0 @@
# graphify: skip vendored / minified third-party assets — not first-party code
**/static/lib/
**/static/src/lib/
**/static/**/*.min.js
*.min.js
*.min.css

View File

@@ -1,133 +0,0 @@
<h2>Recommended Hybrid: A + B's escape hatch</h2>
<p class="subtitle">Layout A's inline badge as default. Power users click "Show alternatives" on any line to reveal B's ranked panel for that line only.</p>
<div class="mockup">
<div class="mockup-header">Bank Reconciliation — Account: RBC Operating · 487 unreconciled</div>
<div class="mockup-body" style="padding:14px;font-family:-apple-system,sans-serif;font-size:13px;background:#f3f4f6">
<div style="background:#fff;border:1px solid #d8dadd;border-radius:8px;margin-bottom:10px;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px">
<div>
<div style="font-weight:600">Apr 12 — RBC e-transfer</div>
<div style="color:#666;font-size:12px;margin-top:2px">Cheque 4827 · Westin Plating Co · <strong>$1,847.50 CAD</strong></div>
</div>
<div style="display:flex;gap:8px;align-items:center">
<div style="background:#22c55e;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700;letter-spacing:0.3px">92% MATCH</div>
</div>
</div>
<div style="padding:10px 14px;background:#f0fdf4;border-top:1px solid #d1fae5;display:flex;justify-content:space-between;align-items:center">
<div style="font-size:12px;color:#166534">
💡 <strong>INV/2026/00123</strong> — Westin Plating Co — $1,847.50
</div>
<div style="display:flex;gap:6px">
<button style="background:#22c55e;color:#fff;border:none;padding:5px 12px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer">Accept</button>
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Reject</button>
<button style="background:transparent;color:#666;border:none;padding:5px 8px;font-size:11px;cursor:pointer;text-decoration:underline">Show 2 alternatives</button>
</div>
</div>
</div>
<div style="background:#fff;border:1px solid #fde68a;border-radius:8px;margin-bottom:10px;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px">
<div>
<div style="font-weight:600">Apr 12 — RBC payment</div>
<div style="color:#666;font-size:12px;margin-top:2px">Cheque 4828 · partner unknown · <strong>$1,800.00 CAD</strong></div>
</div>
<div style="background:#f59e0b;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700">68% MATCH</div>
</div>
<div style="padding:10px 14px;background:#fffbeb;border-top:1px solid #fde68a;display:flex;justify-content:space-between;align-items:center">
<div style="font-size:12px;color:#92400e">
💡 <strong>INV/2026/00098</strong> — Westin Plating Co — $1,800.00 · <em style="color:#a16207">amount matches but partner unconfirmed</em>
</div>
<div style="display:flex;gap:6px">
<button style="background:#f59e0b;color:#fff;border:none;padding:5px 12px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer">Accept</button>
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Reject</button>
<button style="background:transparent;color:#666;border:none;padding:5px 8px;font-size:11px;cursor:pointer;text-decoration:underline">Show 4 alternatives</button>
</div>
</div>
</div>
<div style="background:#fff;border:1px solid #d8dadd;border-radius:8px;margin-bottom:10px;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px">
<div>
<div style="font-weight:600">Apr 11 — Visa adjustment</div>
<div style="color:#666;font-size:12px;margin-top:2px">Ref VSA-201 · Royal Bank fees · <strong>$89.99 CAD</strong></div>
</div>
<div style="background:#94a3b8;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700">NO MATCH</div>
</div>
<div style="padding:8px 14px;background:#f8fafc;border-top:1px solid #e2e8f0;display:flex;gap:6px;justify-content:flex-end">
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Reconcile manually</button>
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Apply rule</button>
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 10px;border-radius:5px;font-size:11px;cursor:pointer">Write off</button>
</div>
</div>
<div style="background:#fff;border:2px solid #22c55e;border-radius:8px;margin-bottom:10px;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:#f0fdf4">
<div>
<div style="font-weight:600">Apr 11 — RBC bulk deposit</div>
<div style="color:#666;font-size:12px;margin-top:2px">Ref 9921-D · Westin Plating Co · <strong>$3,200.00 CAD</strong></div>
</div>
<div style="background:#22c55e;color:#fff;padding:4px 10px;border-radius:14px;font-size:11px;font-weight:700">98% MATCH (alternatives expanded)</div>
</div>
<div style="background:#f8fafc;padding:10px 14px;border-top:1px solid #d1fae5">
<div style="font-size:11px;color:#666;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.4px;font-weight:600">AI suggestions, ranked</div>
<div style="background:#fff;border:1px solid #22c55e;border-radius:6px;padding:8px 10px;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-size:12px;font-weight:600;color:#166534">98% — INV/2026/00145 — $3,200.00 · Westin Plating Co</div>
<div style="font-size:11px;color:#666;margin-top:2px">Exact amount + same partner + invoice date Apr 8 · 4 prior reconciles match this pattern</div>
</div>
<button style="background:#22c55e;color:#fff;border:none;padding:5px 14px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer">Accept</button>
</div>
<div style="background:#fff;border:1px solid #fde68a;border-radius:6px;padding:8px 10px;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-size:12px;font-weight:600;color:#92400e">71% — INV/2026/00141 — $3,200.00 · Bramalea Lift Co</div>
<div style="font-size:11px;color:#666;margin-top:2px">Amount matches, partner is a different client</div>
</div>
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 12px;border-radius:5px;font-size:11px;cursor:pointer">Use this</button>
</div>
<div style="background:#fff;border:1px solid #d8dadd;border-radius:6px;padding:8px 10px;display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-size:12px;font-weight:600;color:#666">62% — INV/2026/00139 + INV/2026/00140 (combined) — Westin Plating Co</div>
<div style="font-size:11px;color:#666;margin-top:2px">Two invoices summing to $3,200.00</div>
</div>
<button style="background:#fff;color:#666;border:1px solid #d8dadd;padding:5px 12px;border-radius:5px;font-size:11px;cursor:pointer">Use this</button>
</div>
<div style="margin-top:6px"><button style="background:transparent;color:#666;border:none;padding:4px;font-size:11px;cursor:pointer;text-decoration:underline">Hide alternatives</button></div>
</div>
</div>
<div style="background:#fff;border:1px solid #d8dadd;border-radius:8px;padding:8px 14px;text-align:center">
<button style="background:#22c55e;color:#fff;border:none;padding:8px 22px;border-radius:6px;font-size:12px;font-weight:700;cursor:pointer">Accept all 47 high-confidence (≥95%)</button>
<span style="color:#666;font-size:11px;margin-left:10px">·</span>
<span style="color:#666;font-size:11px;margin-left:8px">487 lines unreconciled · 47 ready to auto-accept · 134 need review · 306 no AI match</span>
</div>
</div>
</div>
<p class="subtitle">Each line: confidence badge top-right, single suggestion strip below (Accept / Reject / Show alternatives). High-confidence lines have a green border for instant scanning. Bottom bar offers batch-accept of all ≥95% matches at once. The 4th line shows what "Show alternatives" reveals when expanded — B's ranked panel inline.</p>
<div class="options">
<div class="option" data-choice="approve" onclick="toggleSelect(this)">
<div class="letter"></div>
<div class="content">
<h3>Looks right — proceed with this hybrid</h3>
<p>I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.</p>
</div>
</div>
<div class="option" data-choice="adjust" onclick="toggleSelect(this)">
<div class="letter"></div>
<div class="content">
<h3>Mostly right but I want changes</h3>
<p>Tell me in the terminal what to adjust (positions, colours, button labels, missing actions, etc.).</p>
</div>
</div>
<div class="option" data-choice="back_to_pure_a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Just pure A, no alternatives panel</h3>
<p>Keep it simple — single suggestion per line, no expand. If user disagrees with AI they go to the manual reconcile dialog.</p>
</div>
</div>
</div>

View File

@@ -1,101 +0,0 @@
<h2>AI Suggestion Placement</h2>
<p class="subtitle">You picked "AI assistive" — now: how does the AI suggestion appear on each unreconciled bank line? Three layouts:</p>
<div class="cards" data-multiselect>
<div class="card" data-choice="badge_inline" onclick="toggleSelect(this)">
<div class="card-image">
<div class="mockup">
<div class="mockup-header">Layout A — Inline Badge</div>
<div class="mockup-body" style="padding:12px;font-family:monospace;font-size:13px;line-height:1.7">
<div style="border:1px solid #d8dadd;padding:10px;border-radius:6px;background:#fff">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-weight:600">Apr 12 — RBC ETF deposit</div>
<div style="color:#666;font-size:12px">Cheque ref 4827 · $1,847.50 CAD</div>
</div>
<div style="background:#22c55e;color:#fff;padding:3px 8px;border-radius:12px;font-size:11px;font-weight:600">92% MATCH</div>
</div>
<div style="margin-top:8px;padding:6px;background:#f0fdf4;border-left:3px solid #22c55e;font-size:12px;color:#166534">
💡 Invoice <strong>INV/2026/00123</strong> — Westin Plating Co — $1,847.50 · <a href="#" style="color:#22c55e">Accept</a> · <a href="#" style="color:#666">Reject</a>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>A — Inline Badge + Suggestion Strip</h3>
<p>Confidence badge top-right of each line, suggestion strip just below. One-click Accept/Reject. Familiar Enterprise-style line layout, AI feels like a layer added on top.</p>
</div>
</div>
<div class="card" data-choice="side_panel" onclick="toggleSelect(this)">
<div class="card-image">
<div class="mockup">
<div class="mockup-header">Layout B — Side Panel</div>
<div class="mockup-body" style="padding:12px;font-family:monospace;font-size:12px">
<div style="display:flex;gap:8px;height:200px">
<div style="flex:1;border:1px solid #d8dadd;border-radius:6px;background:#fff;padding:8px">
<div style="font-weight:600;margin-bottom:6px">Bank lines</div>
<div style="background:#dbeafe;padding:6px;border-radius:4px;margin-bottom:4px;font-size:11px">Apr 12 RBC $1,847.50 ✓ selected</div>
<div style="padding:6px;font-size:11px;color:#666">Apr 12 RBC $245.00</div>
<div style="padding:6px;font-size:11px;color:#666">Apr 11 Visa $89.99</div>
<div style="padding:6px;font-size:11px;color:#666">Apr 11 RBC $3,200.00</div>
</div>
<div style="width:200px;border:1px solid #d8dadd;border-radius:6px;background:#f8fafc;padding:8px">
<div style="font-weight:600;font-size:11px;margin-bottom:6px">AI Suggestions</div>
<div style="padding:6px;background:#fff;border-radius:4px;margin-bottom:4px;font-size:10px">
<div style="color:#22c55e;font-weight:600">92% INV/2026/00123</div>
<div style="color:#666">Westin Plating $1,847.50</div>
</div>
<div style="padding:6px;background:#fff;border-radius:4px;font-size:10px">
<div style="color:#f59e0b;font-weight:600">68% INV/2026/00098</div>
<div style="color:#666">Westin Plating $1,800.00</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>B — Dedicated Side Panel</h3>
<p>Bank lines on the left, AI suggestions panel on the right, updates as you select a line. Multiple ranked suggestions visible. More screen real estate for AI; line list stays clean.</p>
</div>
</div>
<div class="card" data-choice="hover_only" onclick="toggleSelect(this)">
<div class="card-image">
<div class="mockup">
<div class="mockup-header">Layout C — Hover Reveal</div>
<div class="mockup-body" style="padding:12px;font-family:monospace;font-size:13px;line-height:1.7">
<div style="border:1px solid #d8dadd;padding:10px;border-radius:6px;background:#fff;margin-bottom:6px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-weight:600">Apr 12 — RBC ETF deposit</div>
<div style="color:#666;font-size:12px">Cheque ref 4827 · $1,847.50 CAD</div>
</div>
<div style="display:flex;align-items:center;gap:6px">
<div style="width:8px;height:8px;background:#22c55e;border-radius:50%"></div>
<div style="color:#666;font-size:11px;font-style:italic">hover for AI</div>
</div>
</div>
</div>
<div style="border:1px solid #22c55e;padding:10px;border-radius:6px;background:#f0fdf4;box-shadow:0 4px 12px rgba(0,0,0,0.08)">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<div style="font-weight:600">Apr 12 — RBC e-transfer</div>
<div style="color:#166534;font-size:12px">💡 92% match: INV/2026/00123 — $1,847.50 · <a href="#" style="color:#22c55e">Accept</a></div>
</div>
<div style="background:#22c55e;color:#fff;padding:3px 8px;border-radius:12px;font-size:11px">92%</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>C — Hover-to-Reveal</h3>
<p>Just a confidence dot on each line. AI details appear on hover/click. Cleanest visual, most Enterprise-like density. Slowest discovery for new users.</p>
</div>
</div>
</div>
<p class="subtitle">Click your preferred option(s). I'll read your selection on the next turn. You can also describe in the terminal what you'd like changed.</p>

View File

@@ -1,29 +0,0 @@
<h2>Phase 1 — Bank Reconciliation</h2>
<p class="subtitle">Brainstorming session for the next sub-module: <code>fusion_accounting_bank_rec</code></p>
<div class="section">
<h3>What we're designing</h3>
<p>A native bank-rec widget that replaces Odoo Enterprise's <code>account_accountant</code> bank reconciliation, using Odoo 19's frontend OWL architecture. It reads/writes the same <code>account.partial.reconcile</code> tables Community owns, so existing reconciliations are immune to Enterprise uninstall (verified empirically in Phase 0).</p>
</div>
<div class="section">
<h3>Reference material I've already scanned</h3>
<ul>
<li><strong>Roadmap design</strong> Section 4.3 — Phase 1 scope, exit criteria</li>
<li><strong>Enterprise V19 source</strong> at <code>RePackaged-Odoo/accounting/account_accountant/</code> — 17 OWL components in <code>static/src/components/bank_reconciliation/</code>, 1 service file (140 lines), 3 inherits on community models, 2 wizards</li>
<li><strong>Phase 0 BankRecAdapter</strong> — already present at <code>fusion_accounting_ai/services/data_adapters/bank_rec.py</code> with a stub <code>list_unreconciled_via_fusion()</code> waiting to be filled in</li>
</ul>
</div>
<div class="section">
<h3>How this session works</h3>
<ol>
<li>I ask clarifying questions one at a time (terminal for scope/concept, this browser for layout/visual)</li>
<li>I propose 2-3 architectural approaches with tradeoffs</li>
<li>We work through the design section by section</li>
<li>I write the spec doc and you approve it</li>
<li>Then we transition to writing the implementation plan</li>
</ol>
</div>
<p class="subtitle">Continuing in terminal for the first question...</p>

View File

@@ -1,4 +0,0 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:16px">
<p class="subtitle">UI layout approved ✓</p>
<p class="subtitle">Continuing in terminal — next sections are about file structure, reconcile engine algorithms, and migration. Browser will return for any further visual decisions.</p>
</div>

View File

@@ -1,4 +0,0 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh;flex-direction:column;gap:16px">
<p class="subtitle">Spec approved ✓ — committed as <code>2d64f7e</code></p>
<p class="subtitle">Now writing the Phase 1 implementation plan in terminal. Browser session can be closed; the visual companion isn't needed for plan-writing.</p>
</div>

View File

@@ -1 +0,0 @@
{"reason":"idle timeout","timestamp":1776605003749}

View File

@@ -1,12 +0,0 @@
{"type":"server-started","port":50540,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:50540","screen_dir":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content","state_dir":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/state"}
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/intro.html"}
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/ai-badge-placement.html"}
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/ai-badge-hybrid-v2.html"}
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603091592}
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603096458}
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603097158}
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603097583}
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603097800}
{"source":"user-event","type":"click","text":"✓\n \n Looks right — proceed with this hybrid\n I'll capture this as the default UI design in the spec. Specific colour choices and exact pixel spacing get refined during implementation.","choice":"approve","id":null,"timestamp":1776603098691}
{"type":"screen-added","file":"/Users/gurpreet/Github/Odoo-Modules/.superpowers/brainstorm/84408-1776602183/content/waiting-1.html"}
{"type":"server-stopped","reason":"idle timeout"}

View File

@@ -1,95 +0,0 @@
# Odoo Modules — Codex Instructions
## Project
27 custom Odoo 19 modules for Fusion Central (Westin Healthcare + NEXA Systems).
## Critical Rules — Odoo 19
1. **NEVER code from memory** — Always read a reference file from Docker first:
```bash
docker exec odoo-dev-app cat /usr/lib/python3/dist-packages/odoo/addons/<module>/static/src/<path>
```
2. **Frontend JS**: Use `Interaction` class from `@web/public/interaction`, registered via `registry.category("public.interactions")`. NOT IIFE/DOMContentLoaded.
3. **Backend OWL**: Use standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`. `static props = []` not `{}`.
4. **HTTP routes**: `type="jsonrpc"` — NOT `type="json"` (deprecated).
5. **res.config.settings**: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.
6. **res.groups**: NO `users` field, NO `category_id` field.
7. **Search views**: NO `group expand="0"` syntax.
8. **SCSS imports**: `@import "./partial"` is FORBIDDEN in Odoo 19 custom SCSS. It prints a warning and silently falls back to the old cached bundle. Register every SCSS file (including `_partial.scss` tokens) as a separate entry in `web.assets_backend`. Put tokens first; Odoo concatenates bundle files so SCSS variables/mixins from the first file are visible to every later file.
## Card Styling — Copy Odoo's Kanban Pattern
Don't rely on `var(--bs-border-color)` or `var(--bs-body-bg)` for card surfaces — they drift between themes/addons and often render **invisible**. Odoo's own kanban (`.o_kanban_record`) uses **explicit hex** values:
```css
background-color: white;
border: 1px solid #d8dadd;
```
For custom OWL dashboards / client actions use the same approach:
- Define a `_tokens.scss` partial with explicit hex values wrapped in a CSS custom property:
```scss
$fp-card: var(--fp-card-bg, #ffffff);
$fp-border: var(--fp-border-color, #d8dadd);
```
- Reference those tokens everywhere (never `var(--bs-border-color)` directly)
- Three-layer contrast: **page** (grayest) → **container/column** (mid) → **card** (brightest). That's what makes cards pop.
- Reference implementation: `fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss`.
## Dark Mode — Branch on `$o-webclient-color-scheme` at SCSS Compile Time
Odoo 19 does NOT flip dark mode via a runtime DOM class. It compiles TWO asset bundles:
- `web.assets_backend` — compiled with `$o-webclient-color-scheme: bright`
- `web.assets_web_dark` — compiled with `$o-webclient-color-scheme: dark` (dark variant primary variables loaded first)
Your SCSS file is compiled into BOTH bundles. To make the dark bundle have different colors, **branch at compile time** using the SCSS variable Odoo sets:
```scss
$o-webclient-color-scheme: bright !default;
$_my-page-hex: #f3f4f6;
$_my-card-hex: #ffffff;
@if $o-webclient-color-scheme == dark {
$_my-page-hex: #1a1d21 !global;
$_my-card-hex: #22262d !global;
}
$my-page: var(--my-page-bg, $_my-page-hex);
$my-card: var(--my-card-bg, $_my-card-hex);
```
**Do NOT use** `.o_dark_mode` class selectors, `[data-bs-theme="dark"]`, or `@media (prefers-color-scheme: dark)` — none of those fire reliably in Odoo 19. The user toggles dark mode via the user profile, which sets a `color_scheme` cookie and reloads the page; Odoo then serves the dark bundle. Your SCSS `@if` handles the rest at compile time.
Verify by inspecting the attachments — you should see two files with different URLs for the two bundles:
```python
env['ir.qweb']._get_asset_bundle('web.assets_backend').css() # light
env['ir.qweb']._get_asset_bundle('web.assets_web_dark').css() # dark
```
## Asset Bundle Cache Busting
Odoo content-hashes the compiled bundle URL (`/web/assets/<hash>/...`). When CSS changes but the hash doesn't update, the browser serves the old bundle. Fixes in order of escalation:
1. Bump the module `version` in `__manifest__.py`
2. `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';` then restart odoo
3. Call `env['ir.qweb']._get_asset_bundle('web.assets_backend').css()` in odoo-shell to force regeneration
4. Hard-refresh browser with cache clear (DevTools → right-click refresh → *Empty Cache and Hard Reload*); on mobile clear website data
## Naming
- New fields: `x_fc_*` prefix
- Legacy fields: `x_studio_*`
- Canadian English for all user-facing text
- Currency: `$` sign with Monetary fields + currency_id
## Cursor-Managed Modules
- **fusion_clock** is currently being modified in Cursor — always read files fresh before editing, don't assume you know the current state
- **fusion_repairs** — status and deferred work: [`fusion_repairs/cloud.md`](fusion_repairs/cloud.md) (bundles 111 shipped at `19.0.2.2.4`; not production-deployed)
## Workflow
- Local dev: `docker exec odoo-dev-app odoo -d fusion-dev -u <module> --stop-after-init`
- Local URL: http://localhost:8069
- Test before deploying. Edit existing files — don't create unnecessary new ones.
## Supabase Knowledge Base
Before starting unfamiliar work, check Supabase for context:
```bash
PGPASSWORD='a09e12e0995dc29446631fa458f3d4b3' psql -h 100.74.28.73 -p 5433 -U postgres -d postgres
```
- `fusionapps.decisions` — past architecture decisions
- `fusionapps.issues` — known issues and fixes
- `fusionapps.code_snippets` — reference code
- `fusionapps.quick_commands` — deployment and admin commands

294
CLAUDE.md
View File

@@ -1,294 +0,0 @@
# Odoo Modules — Claude Code Instructions
## Project
27 custom Odoo 19 modules for Fusion Central (Westin Healthcare + NEXA Systems).
## Critical Rules — Odoo 19
1. **NEVER code from memory** — Always read a reference file from Docker first:
```bash
docker exec odoo-dev-app cat /usr/lib/python3/dist-packages/odoo/addons/<module>/static/src/<path>
```
2. **Frontend JS**: Use `Interaction` class from `@web/public/interaction`, registered via `registry.category("public.interactions")`. NOT IIFE/DOMContentLoaded.
3. **Backend OWL**: Use standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`. `static props = []` not `{}`.
4. **HTTP routes**: `type="jsonrpc"` — NOT `type="json"` (deprecated).
5. **res.config.settings**: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.
**`config_parameter=` Boolean fields don't round-trip `False` as a string.** Odoo's `set_values()` calls `IrConfigParameter.set_param(key, value)`, and `set_param` deletes the row when `value` is falsy (False / None / empty). So writing `False` to a Boolean config field means the param no longer exists in `ir_config_parameter`; a subsequent `get_param(key)` returns the *default* (Python `False`), not `'False'`. Test like `self.assertFalse(ICP.get_param('...'))` — never `assertEqual(..., 'False')`. (Integer/Float/Char go through `repr(value)` / strip, so they DO persist as strings — `'90'`, `'0'`, etc.) Source: `odoo/addons/base/models/res_config.py::set_values` and `ir_config_parameter.py::set_param`.
6. **res.groups**: NO `users` field, NO `category_id` field. **The Odoo 19 replacement for `category_id` is `res.groups.privilege`.** To make a module's groups appear as application-access dropdowns on the user form (Settings → Users → *Application Accesses*) instead of only in developer mode: define an `ir.module.category`, a `res.groups.privilege` (with `category_id` → that category), and set each group's `privilege_id` → that privilege. Groups under one privilege that form an `implied_ids` chain render as a single role dropdown; a standalone group in its own privilege renders as a separate row under the same category header. Verified in `fusion_clock/security/security.xml`; mirrors `fusion_plating`/`fusion_tasks`.
**res.users**: field was renamed `groups_id` → `group_ids` (also `all_group_ids` for implied). The plural form is gone; using `groups_id` raises `ValueError: Invalid field 'groups_id' in 'res.users'`.
**`ir.ui.view`**: same rename — view-level visibility gating uses `group_ids`, not `groups_id`. A record like `<field name="groups_id" eval="[(4, ref('base.group_system'))]"/>` on an `ir.ui.view` raises `ValueError: Invalid field 'groups_id' in 'ir.ui.view'` at module install. (The XML *attribute* `groups="base.group_system"` on form elements like `<page>`, `<button>`, `<field>` is unrelated and still works.)
**`ir.rule` `groups` field is additive, not restrictive.** A rule with `groups=[some_group]` applies ONLY to users in that group — it does NOT restrict non-members. So `domain_force=[(1,'=',1)]` + `groups=[base.group_system]` does NOT mean "only admins see rows"; it means "admins see all rows (and the rule is silent on everyone else)". Non-admins are gated by the ACL (`ir.model.access.csv`), not the rule. To truly restrict by group at the rule layer, pair a global rule (`groups=[]`, `domain_force=[(0,'=',1)]` = block-all baseline) with a group-scoped allow rule. Default to letting the ACL do the gating; use rules for row-level filters that ACLs cannot express.
7. **Search views**: NO `group expand="0"` syntax.
8. **SCSS imports**: `@import "./partial"` is FORBIDDEN in Odoo 19 custom SCSS. It prints a warning and silently falls back to the old cached bundle. Register every SCSS file (including `_partial.scss` tokens) as a separate entry in `web.assets_backend`. Put tokens first; Odoo concatenates bundle files so SCSS variables/mixins from the first file are visible to every later file.
9. **SQL constraints & indexes**: Odoo 19 dropped `_sql_constraints = [(name, def, msg), ...]` and the `init()`/raw-SQL pattern. Both still parse but only emit a warning and are silently ignored. Use declarative class attributes instead:
```python
_check_qty_positive = models.Constraint('CHECK (qty > 0)', 'Quantity must be positive.')
_user_time_idx = models.Index('(user_id, event_time DESC)')
```
The attribute name after the leading underscore becomes the SQL object name suffix (`{table}_{suffix}`). `models.Index` accepts `DESC`, `WHERE` predicates, and `USING btree (...)`. Sources: `odoo/orm/model_classes.py` (warns at registry build), `odoo/orm/table_objects.py` (Constraint + Index classes).
10. **`res.users._login` is an instance method in Odoo 19**, not a classmethod as in earlier versions. Signature is `def _login(self, credential, user_agent_env)` — there is no `db` parameter. Override it like any normal instance method (`super()._login(credential, user_agent_env)`). When called via `authenticate()` on an empty recordset, `self` carries the right env. Older recipes that build a separate `api.Environment` from `odoo.modules.registry.Registry(db)` no longer apply. Source: `odoo/addons/base/models/res_users.py:760`.
11. **Inherited `ir.ui.view` records cannot have `groups`/`group_ids` on the record itself.** Odoo 19 raises `ParseError: Inherited view cannot have 'groups' defined on the record. Use 'groups' attributes inside the view definition` at install time. Move the gate to the inner XML nodes — every `<button>`, `<page>`, `<field>`, `<xpath>`, `<group>` etc. supports a `groups="base.group_system"` attribute. For an inherited form with a smart button + admin tab, put `groups=` on the button and the page individually; leave the `<record model="ir.ui.view">` clean.
12. **`mail.template` QWeb/inline_template `ctx` IS `self.env.context`** — not a nested dict you can pass. `MailRenderMixin._render_eval_context()` sets `ctx = self.env.context`, so `ctx.get('foo')` in subject/body resolves to `env.context.get('foo')`. To pass dynamic data to a template, spread keys directly into the context: `tmpl.with_context(**my_data).send_mail(res_id, ...)`. Calling `tmpl.with_context(ctx=my_data)` puts the dict at `env.context['ctx']`, and the template's `ctx.get('foo')` becomes `env.context.get('foo')` → `None` (looks like a silent rendering bug — subject ends up blank).
13. **`ir.cron` dropped `numbercall`** in Odoo 19. Old recipes set `<field name="numbercall">-1</field>` for "run forever"; that now raises `ValueError: Invalid field 'numbercall' in 'ir.cron'` at install time. Just omit the field — recurring crons keep running as long as `active=True`. Source: `odoo/addons/base/models/ir_cron.py` field list.
14. **`cr.commit()` / `cr.rollback()` raise AssertionError inside `TransactionCase`** — they are NOT silent no-ops in Odoo 19. The test cursor explicitly refuses both ("Cannot commit or rollback a cursor from inside a test, this will lead to a broken cursor when trying to rollback the test. Please rollback to a specific savepoint instead..."). For cron/worker code that needs per-row isolation so one bad row doesn't roll back the whole batch, use `with self.env.cr.savepoint(): ...` inside the loop instead of `cr.commit()`. Savepoints work in both prod (under the outer cron transaction) and tests (under the outer test transaction). The cron transaction commits the whole batch when the method returns; in tests everything rolls back cleanly. Source: `odoo/sql_db.py::TestCursor.commit` and `Cursor.savepoint()`.
15. **There is NO `sale.subscription` model in Odoo 19** (Enterprise `sale_subscription`). A subscription is a **`sale.order`** with `is_subscription=True`, `plan_id` → **`sale.subscription.plan`** (the recurrence), plus `subscription_state` / `next_invoice_date` / `recurring_monthly`. Any Many2one or relation that targets "a subscription" must point at `sale.order` (filter `domain=[('is_subscription','=',True)]`) — **not** `sale.subscription`, which does not exist and fails at install. The surviving `sale.subscription.*` records are only the plan + wizards/reports (`sale.subscription.plan`, `sale.subscription.report`, `sale.subscription.change.customer.wizard`, `sale.subscription.close.reason.wizard`). Verified on live `nexamain` (odoo-nexa, 19.0): `SELECT model FROM ir_model WHERE model LIKE 'sale.subscription%'`.
16. **Renaming a module's technical name needs a DB rename, not just a folder rename.** The technical name is baked into the database: `ir_module_module.name`, every external ID in `ir_model_data.module`, each view's `ir_ui_view.key` prefix, and the `ir_module_module_dependency.name` rows of every module that depends on it. Rename only the folder + in-code references and Odoo treats the new name as a fresh uninstalled module — installing it **duplicates** groups/templates/menus and **orphans** all existing data. On every DB that already has it installed, run an in-place SQL rename (the 4 tables above) **before** `-u <newname>`; a fresh DB needs nothing. Reference script + full rationale: [`fusion_portal/rename_module.sql`](fusion_portal/rename_module.sql) (written for the `fusion_authorizer_portal` → `fusion_portal` rename). Also update cross-module `depends`, `inherit_id="<old>.view"`, `t-call`, `env.ref('<old>.xmlid')`, asset paths (`<old>/static/...`), and `from odoo.addons.<old>... import`.
17. **`url_encode` (and werkzeug url helpers) are NOT available in the Odoo 19 `mail.template` QWeb render context.** Using `url_encode({...})` inside a template `body_html` (e.g. to build a fallback link) makes the template fail Odoo's save-time render validation **at install**, surfacing as the opaque `ParseError: ... Oops! We couldn't save your template due to an issue with this value: <the entire body html>` (the real `NameError` is hidden, and `--log-handler odoo.tools.convert:DEBUG` does NOT reveal it). Build URLs with plain string methods instead: `'https://…?q=' + (value or '').replace(' ', '+')`. Found installing `fusion_repairs` (post-visit NPS template). **That same opaque "issue with this value" error wraps ANY render failure in a mail.template body** — when you see it, suspect an undefined name / bad field reference in the template, not malformed XML.
## Card Styling — Copy Odoo's Kanban Pattern
Don't rely on `var(--bs-border-color)` or `var(--bs-body-bg)` for card surfaces — they drift between themes/addons and often render **invisible**. Odoo's own kanban (`.o_kanban_record`) uses **explicit hex** values:
```css
background-color: white;
border: 1px solid #d8dadd;
```
For custom OWL dashboards / client actions use the same approach:
- Define a `_tokens.scss` partial with explicit hex values wrapped in a CSS custom property:
```scss
$fp-card: var(--fp-card-bg, #ffffff);
$fp-border: var(--fp-border-color, #d8dadd);
```
- Reference those tokens everywhere (never `var(--bs-border-color)` directly)
- Three-layer contrast: **page** (grayest) → **container/column** (mid) → **card** (brightest). That's what makes cards pop.
- Reference implementation: `fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss`.
## Dark Mode — Branch on `$o-webclient-color-scheme` at SCSS Compile Time
Odoo 19 does NOT flip dark mode via a runtime DOM class. It compiles TWO asset bundles:
- `web.assets_backend` — compiled with `$o-webclient-color-scheme: bright`
- `web.assets_web_dark` — compiled with `$o-webclient-color-scheme: dark` (dark variant primary variables loaded first)
Your SCSS file is compiled into BOTH bundles. To make the dark bundle have different colors, **branch at compile time** using the SCSS variable Odoo sets:
```scss
$o-webclient-color-scheme: bright !default;
$_my-page-hex: #f3f4f6;
$_my-card-hex: #ffffff;
@if $o-webclient-color-scheme == dark {
$_my-page-hex: #1a1d21 !global;
$_my-card-hex: #22262d !global;
}
$my-page: var(--my-page-bg, $_my-page-hex);
$my-card: var(--my-card-bg, $_my-card-hex);
```
**Do NOT use** `.o_dark_mode` class selectors, `[data-bs-theme="dark"]`, or `@media (prefers-color-scheme: dark)` — none of those fire reliably in Odoo 19. The user toggles dark mode via the user profile, which sets a `color_scheme` cookie and reloads the page; Odoo then serves the dark bundle. Your SCSS `@if` handles the rest at compile time.
Verify by inspecting the attachments — you should see two files with different URLs for the two bundles:
```python
env['ir.qweb']._get_asset_bundle('web.assets_backend').css() # light
env['ir.qweb']._get_asset_bundle('web.assets_web_dark').css() # dark
```
## Asset Bundle Cache Busting
Odoo content-hashes the compiled bundle URL (`/web/assets/<hash>/...`). When CSS changes but the hash doesn't update, the browser serves the old bundle. Fixes in order of escalation:
1. Bump the module `version` in `__manifest__.py`
2. `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';` then restart odoo
3. Call `env['ir.qweb']._get_asset_bundle('web.assets_backend').css()` in odoo-shell to force regeneration
4. Hard-refresh browser with cache clear (DevTools → right-click refresh → *Empty Cache and Hard Reload*); on mobile clear website data
## Naming
- New fields: `x_fc_*` prefix
- Legacy fields: `x_studio_*`
- Canadian English for all user-facing text
- Currency: `$` sign with Monetary fields + currency_id
## Module-Specific Notes
- **fusion_clock** — developed in **Claude Code** (no longer Cursor; no concurrent-editing conflicts). Changed a lot recently (NFC kiosk: tap-to-clock, enrollment + program-from-unknown-tap, manager page, sounds, screen lock, guided profile-photo capture, faster animations). Still read files fresh before editing rather than assuming the layout. Live on entech (`odoo-entech` / LXC 111 on `pve-worker5`).
- **fusion_repairs** — read [`fusion_repairs/cloud.md`](fusion_repairs/cloud.md) before feature work. **Version `19.0.2.3.0`** (Plan-1 maintenance foundation added 2026-06-02). **NOT Community-installable** — it transitively pulls in Enterprise `ai` + `knowledge` (`fusion_repairs → fusion_portal → fusion_claims → ai`; `fusion_portal → knowledge`), so it can NOT be installed or tested on local `odoo-modsdev` (Community) — the old `-d fusion-dev -u fusion_repairs` recipe does NOT work. **Test on Enterprise:** an isolated `westin-fr-test` DB on the `odoo-westin` host (clone of prod `westin-v19`; a fresh-DB clone install also needs a one-time orphaned-FK cleanup because prod has orphaned account/tax m2m rows). First-ever clean install surfaced + fixed 2 bugs (url_encode → rule 17; menu parent defined after its children) in commit `903ceb10`. **Not production-deployed** to Westin yet. **Test-runner gotchas on that prod-config container:** `--test-enable` SILENTLY SKIPS all tests without `--workers 0`; the conf's `log_level=warn` hides test output (add `--log-level=test`); the post_install phase also trips on a pre-existing module, so verify behaviour via `odoo shell` rather than the test runner. `mail_template_data.xml` is `noupdate=1` → template edits load on a FRESH install (the prod deploy) but NOT on `-u` of an already-installed DB. Outstanding: maintenance booking (Plan 2), visit log (Plan 3), backfill wizard (Plan 4), office follow-up crons (Plan 5), RingCentral SMS.
- **fusion_portal** (formerly `fusion_authorizer_portal`) — authorizer/sales-rep portal; **ENTERPRISE-only** (depends `knowledge` → cannot run on local Community; verify on a westin clone, see *Westin Prod* below). **Assessment-visit flow LIVE on westin, v19.0.2.10.1.** A `fusion.assessment.visit` bundles the assessments from one home visit and, on completion (`action_complete_visit`), groups them by funding workflow (`x_fc_sale_type`) into ONE draft sale order per workflow (MoD/ADP/ODSP/WSIB/private/hardship/insurance) — never one combined SO, never one-per-item-within-a-funding. ADP devices group into one order (combination guard: ≤1 seated {wheelchair/powerchair/scooter} + ≤1 walker); accessibility items group per funding. Reps enter via the "Start a Visit" dashboard tile → `/my/visit/new`; the express/accessibility forms carry `?visit_id=` and defer SO creation to the visit. Renaming the technical name needs a DB rename — see [`fusion_portal/rename_module.sql`](fusion_portal/rename_module.sql).
## Workflow
- Local dev: `docker exec odoo-modsdev-app odoo -d fusion-dev -u <module> --stop-after-init`
- Local URL: http://localhost:8082
- **Running module tests requires ephemeral ports.** The dev container's main Odoo process holds 8069 and 8072; a `docker exec ... odoo --test-enable` will die with `Address already in use` unless you also pass `--http-port=0 --gevent-port=0`. This is because Odoo 19 forces `http_spawn()` when `--test-enable` is set, even when `--no-http` is passed. Canonical test invocation:
```bash
docker exec odoo-modsdev-app odoo -d fusion-dev --test-enable --test-tags /<module> \
-u <module> --stop-after-init --http-port=0 --gevent-port=0 2>&1 | tail -60
```
- **`fusion_centralize_billing` tests run on odoo-trial (VM 316).** Local dev is Community and cannot install this module. Use `bash scripts/fcb_test_on_trial.sh` from the repo root. The script uses `--http-port 8070` to avoid the port 8069 conflict with the live odoo-trial-app container. Pass = `FCB_EXIT=0`. Takes ~1-2 min.
- **Python deps not bundled with `odoo:19` image:** `user_agents` (used by `fusion_login_audit`), and likely others. Install ephemerally with `docker exec -u 0 odoo-modsdev-app pip install <pkg> --break-system-packages`. The install is LOST when the container is recreated (e.g. `docker compose up -d` after a compose edit). When this happens, the symptom is `ModuleNotFoundError` deep in the auth or report code. Re-run the pip install. A persistent fix would be a custom Dockerfile or a startup hook on the compose service — not done yet.
- Test before deploying. Edit existing files — don't create unnecessary new ones.
## PDF Preview — Prefer fusion_pdf_preview Over Downloads/New-Tab
When a Python action opens an attachment, route it through `fusion_pdf_preview` instead of returning `ir.actions.act_url` with `download=true` or `target=new`. The preview dialog gives operators preview + print + download in one place and writes an audit log; non-PDF attachments fall back to the legacy download path automatically.
The drop-in replacement is the new helper on `ir.attachment`:
```python
return att.action_fusion_preview(title='My Doc')
# vs. the old pattern:
# return {'type': 'ir.actions.act_url',
# 'url': '/web/content/%s?download=true' % att.id,
# 'target': 'new'}
```
The helper auto-detects mimetype: PDFs go to the dialog, everything else (ZPL, CSV, XML, images) stays on download. So a callsite that today serves CSV today and a PDF tomorrow doesn't need a code change — same call, different routing.
If you need to invoke the client action directly (rare — only when you don't have a recordset handy), the tag is `fusion_pdf_preview.open_attachment` and the params are `{attachment_id, title, model_name, record_ids, report_name}`. See `fusion_pdf_preview/static/src/js/open_attachment_action.js`.
Existing reports (`ir.actions.report` of type `qweb-pdf`) are intercepted automatically by `fusion_pdf_preview/static/src/js/pdf_preview.js`; the helper above is for the *other* pattern — attachments opened by custom buttons.
## Supabase Knowledge Base
Before starting unfamiliar work, check Supabase for context:
```bash
PGPASSWORD='a09e12e0995dc29446631fa458f3d4b3' psql -h 100.74.28.73 -p 5433 -U postgres -d postgres
```
- `fusionapps.decisions` — past architecture decisions
- `fusionapps.issues` — known issues and fixes
- `fusionapps.code_snippets` — reference code
- `fusionapps.quick_commands` — deployment and admin commands
## Westin Prod — Deploy & Clone-Verify (fusion_portal et al.)
Westin prod: host `odoo-westin`, app container `odoo-dev-app`, db container `odoo-dev-db`, DB `westin-v19` (user `odoo`, pw `DevSecure2025!`), addons `/opt/odoo/custom-addons` → `/mnt/extra-addons`, Enterprise `/mnt/enterprise-addons`, conf `/etc/odoo/odoo.conf`. ENTERPRISE env — modules depending on `knowledge` (fusion_portal → fusion_claims) cannot run on local Community, so verify on a clone before prod.
**Clone-verify a change (prod-safe, isolated — prod files + live DB untouched):**
1. Clone online: `docker exec -e PGPASSWORD='DevSecure2025!' odoo-dev-db sh -c 'dropdb -U odoo --if-exists westin-v19-visittest; createdb -U odoo -O odoo westin-v19-visittest && pg_dump -U odoo westin-v19 | psql -U odoo -q -d westin-v19-visittest'` (~2 min, ~152M -Fc).
2. Stage the branch module into an isolated dir INSIDE the addons path: `/opt/odoo/custom-addons/_test/<module>`, then `-u <module> --stop-after-init --no-http --db_host db --db_port 5432 --db_user odoo --db_password 'DevSecure2025!' --addons-path=/usr/lib/python3/dist-packages/odoo/addons,/usr/lib/python3/dist-packages/addons,/mnt/extra-addons/_test,/mnt/enterprise-addons,/mnt/extra-addons`. The `/mnt/extra-addons/_test` prefix SHADOWS prod's copy (first matching path wins); deps load from the real `/mnt/extra-addons`.
3. Smoke-test via `odoo shell -d westin-v19-visittest` (same addons-path); `env.cr.rollback()` at the end. To exercise email paths WITHOUT sending: `UPDATE ir_mail_server SET active=false;` AND in the shell `env['ir.mail_server'].__class__.send_email = lambda self, message, *a, **k: 'noop'` (`odoo shell` rejects `--smtp-server`).
**THE ORPHANED-TAX-FK TRAP** (cost real diagnosis time): westin-v19 has ~3300 orphaned rows in `product_taxes_rel` + ~3300 in `product_supplier_taxes_rel` (`tax_id` → deleted `account_tax`), under FKs that are `convalidated=true` (taxes deleted via an FK-bypassing path; PG never re-checks a validated constraint). A plain `pg_dump | psql` clone can't recreate a *validating* FK over orphaned data → the FK is lost on the clone → Odoo `check_foreign_keys` tries to add it → `ForeignKeyViolation: Key (tax_id)=(N) is not present in account_tax` → "Failed to load registry". **Fix ON THE CLONE only — and the trap is NO LONGER tax-only (2026-06-12: 13 FKs across 9 tables — company/journal/tax/fiscal-position/payslip orphans from past force-deletions).** Diff the FKs and clean exactly what's missing:
```bash
# on each DB: SELECT conrelid::regclass||'|'||conname FROM pg_constraint WHERE contype='f'
# sort both lists, comm -23 prod clone -> every FK that failed to restore
# per missing FK: DELETE rows whose column is NOT IN the referenced table's ids
# (exception: mail_message.record_company_id is SET-NULL semantics -> UPDATE ... SET NULL)
``` **Prod `-u` is SAFE without touching the orphans** — prod's FK already exists, so Odoo skips it (it never re-validates a present FK); proven empirically by replicating FK-present+orphan on a clone and running `-u` (exit 0, orphan untouched). Owner is auditing the orphans — do NOT delete them on prod without sign-off.
**Deploy:** backup (`docker exec ... pg_dump -Fc -U odoo westin-v19 > /opt/odoo/backups/<name>.dump` + `cp -r` the module dir to `/opt/odoo/backups/` — OUTSIDE the addons path, never a `*.bak` dir inside it) → `scp` branch to `/opt/odoo/staging/<module>` → swap into `/opt/odoo/custom-addons/<module>` → `-u <module>` → `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%'` → `docker restart odoo-dev-app`. **Gate the restart on `-u` exit 0**; on failure restore the dir backup and do NOT restart. When a feature branch predates main's other merges, merge to `main` **surgically** (temp worktree off `origin/main` + `git checkout <branch> -- <module>` → commit → fast-forward push) so you don't revert parallel sessions' work.
## Fusion Helpdesk — Customer Follow-up + Embedded Inbox (deployment + handoff)
Two modules: **`fusion_helpdesk`** (client — runs on each client deployment, e.g. entech)
and **`fusion_helpdesk_central`** (runs on the central Odoo = nexa). The client forwards
tickets to central over **XML-RPC**; central find-or-creates the customer partner +
follower; the client shows a server-side-scoped "My Tickets" inbox + systray unread badge.
### Where each runs / how to deploy
- **Central = nexa** (`erp.nexasystems.ca`, VM 315 on pve-worker1, Docker, DB `nexamain`).
Source on host: `/opt/odoo/custom-addons/fusion_helpdesk_central`. Upgrade (brief downtime):
```bash
ssh pve-worker1 "qm guest exec 315 --timeout 590 -- bash -c 'docker stop odoo-nexa-app; docker run --rm --network odoo_odoo-network -v odoo_odoo-data:/var/lib/odoo -v /opt/odoo/custom-addons:/mnt/extra-addons -v /opt/odoo/enterprise-addons:/mnt/enterprise-addons -v /opt/odoo/odoo.conf:/etc/odoo/odoo.conf odoo-nexa:19 odoo -d nexamain -u fusion_helpdesk_central --stop-after-init --http-port=0 --gevent-port=0 > /tmp/up.log 2>&1; docker start odoo-nexa-app'"
```
Use `;` (not `&&`) before `docker start` so the app ALWAYS restarts even if the upgrade
fails. nexa `odoo.conf` has `log_level=warn`, so test/INFO lines are suppressed — verify
the result via DB query, not the upgrade log.
- **Client = entech** (LXC 111 on pve-worker5, **native systemd `odoo.service`**, DB `admin`,
config `/etc/odoo/odoo.conf`, source `/mnt/extra-addons/custom/fusion_helpdesk`). No host
bind mount — get files in with `scp` to pve-worker5 then `pct push 111 <file> <dest>`.
Upgrade as the `odoo` user (NOT root):
```bash
pct exec 111 -- bash -lc "systemctl stop odoo; runuser -u odoo -- /usr/bin/odoo --config /etc/odoo/odoo.conf -d admin -u fusion_helpdesk --stop-after-init --http-port=0 --gevent-port=0 --logfile=/tmp/up.log; systemctl start odoo"
```
**Backup dir MUST live OUTSIDE the addons path** (e.g. `/root/`). A dir named `*.bak.*`
*inside* `/mnt/extra-addons/custom` makes Odoo try to load it as a module →
`FileNotFoundError: Invalid module name: fusion_helpdesk.bak.predeploy` → whole registry
load fails. (Learned the hard way; auto-rollback restored it.) Current rollback copy:
`/root/fh_bak_predeploy`.
### REQUIRED prerequisite on the central service account (easy to miss)
The keystone passes `partner_email`, so central find-or-creates the partner. The XML-RPC
service account (**`support@nexasystems.ca`, uid 33** on nexa) MUST have the **Contact
Creation** group (`base.group_partner_manager`). Without it, `helpdesk.ticket.create`
faults with *"not allowed to create 'Contact' (res.partner)"* for any reporter who isn't
already a contact. Granted on nexa 2026-05-27. **Every new client deployment needs this
grant on the central account.**
### Testing lesson
Client logic (scope domain, seen model, vals, `_norm_email`) is unit-tested in
`fusion_helpdesk/tests/` and runs on local Community (`-d modsdev`). **Smoke tests must
call the controller endpoints, not re-implement their logic** — the Phase 6 smoke test
replicated `build_scope_domain` directly and so missed a `NameError` (`_norm_email`
referenced but never imported) that broke every inbox endpoint. Run
`docker exec odoo-modsdev-app python3 -m pyflakes <file>` after editing controllers — it
catches undefined names instantly.
### Two non-obvious gotchas the first ship hit (fixed 2026-05-27 afternoon)
1. **`group_reporter_admin` had zero members on install** — `res.groups` doesn't auto-grant
to the deployment admin, so the "All (deployment)" toggle never appeared and admins were
stuck with the per-user `partner_email` filter. Fix lives in
`fusion_helpdesk/security/fusion_helpdesk_groups.xml`: extend `base.group_system.implied_ids`
with `(4, ref('fusion_helpdesk.group_reporter_admin'))`. The (4, id) tuple is additive — it
never replaces base's existing implied groups. Verified live: all six entech
`base.group_system` members now return True for
`has_group('fusion_helpdesk.group_reporter_admin')` after the upgrade.
2. **Historical tickets had NULL `x_fc_client_label` + NULL `partner_email`** — anything
created before the customer-followup ship was invisible in "My Tickets" because the scope
filter requires both fields. The reporter identity was preserved only in the description
HTML (the diag block's "User" row). Backfill recipe (50 ENTECH + 1 WESTIN, all in one
transaction):
```sql
UPDATE helpdesk_ticket
SET x_fc_client_label = substring(name from '^\[([A-Z]+)\]'),
partner_email = lower(substring(
substring(description from 'User</td><td[^>]*><code>([^<]+)</code>')
from ', ([^)]+)\)')),
partner_name = regexp_replace(
substring(description from 'User</td><td[^>]*><code>([^<]+)</code>'),
' \(#\d+, [^)]+\)$', '')
WHERE name ~ '^\[[A-Z]+\]'
AND description ~ 'User</td>'
AND x_fc_client_label IS NULL;
```
Safe: SQL UPDATE bypasses the central `helpdesk.ticket.create` override, so no duplicate
ack emails. Per-deployment label inferred from the `[XXX]` name prefix the old code was
already adding. Note: users whose `login != email` (e.g. uid=2 on entech has login
`gsinghpal@outlook.com` and email `gs@nexasystems.ca`) get tagged with their *login* in
backfill — they won't see their old tickets in "Mine", only in "All". New tickets are
tagged with the profile email (`user.email` first, `user.login` fallback).
### STATUS (handoff 2026-05-27 afternoon)
- **Merged to `main`** as squash commit `6c15a7b1` (initial ship). Today's followup is the
group/backfill fix described above — committed separately.
- **Deployed live**: nexa `fusion_helpdesk_central` **19.0.1.1.0**; entech `fusion_helpdesk`
**19.0.1.5.0** (bumped from 19.0.1.4.1 for the implied_ids fix). Both services healthy.
- **Historical entech tickets backfilled** on nexa (51 rows: 50 ENTECH + 1 WESTIN).
- **Smoke-tested live end-to-end** (entech→nexa): partner resolved + follower + `ENTECH`
label, branded ack email queued, support reply visible in thread, inbox scope finds own
ticket, no cross-deployment leak. The "Mine" view for non-admins and the "All" view for
the entech owner both populate as expected.
- **Browser confirmation**: hard-refresh entech (DevTools → Empty Cache and Hard Reload),
open the systray helpdesk dialog. The Mine/All toggle appears for the owner; "All" shows
all 50 ENTECH tickets, "Mine" shows the count matching the owner's profile email.
Tracebacks live in `/var/log/odoo/odoo-server.log` on entech (LXC 111 / pve-worker5).
## Fusion Centralized Billing (`fusion_centralize_billing`) — engine + test harness
Odoo (`odoo-nexa`, live DB `nexamain`) is being made the single billing brain for every
NexaSystems app (NexaCloud, NexaDesk/Fusion-Chat, NexaMaps), **superseding Lago**. The
module adds only the metering + integration layer (service registry, identity links,
metric/charge catalog, aggregate-push usage engine, inbound Lago-shaped REST API at
`/api/billing/v1/*`, outbound HMAC webhooks, dual-run reconciliation); all financial
behaviour is native Odoo **Enterprise** (`sale_subscription` + `payment_stripe` +
`account_accountant`). Design + rollout live in `docs/superpowers/specs/`
(`2026-05-27-nexa-billing-centralized-design.md` = architecture;
`2026-06-02-nexacloud-odoo-billing-cutover-design.md` = NexaCloud pilot: build → import →
dual-run → gated flip) and `docs/superpowers/plans/`.
**Testing it — NOT on local `odoo-modsdev` (community) and NEVER `-u` against live `nexamain`.**
It needs Enterprise deps, so tests run on `odoo-nexa` in an **isolated throwaway container**
against a **fresh** DB with the Canadian localization:
```
ssh odoo-nexa
# fresh DB (inside odoo-nexa-db): dropdb --if-exists fcb_test; createdb fcb_test
cp -a /opt/odoo/custom-addons /opt/odoo/custom-addons-staging # edit/sync HERE, never the live module dir
docker run --rm --network odoo_odoo-network \
-v /opt/odoo/custom-addons-staging:/mnt/extra-addons:ro -v /opt/odoo/enterprise-addons:/mnt/enterprise-addons:ro \
-v /opt/odoo/odoo.conf:/etc/odoo/odoo.conf:ro -v /opt/odoo/staging-data:/var/lib/odoo \
odoo-nexa:19 -c /etc/odoo/odoo.conf -d fcb_test --db_host=db --db_user=odoo \
--addons-path=/usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons,/mnt/enterprise-addons \
--without-demo=all --test-enable --test-tags /fusion_centralize_billing \
-i l10n_ca,fusion_centralize_billing --stop-after-init --no-http
```
Iterate with `-u fusion_centralize_billing` (reuse fcb_test). Gotchas that cost hours:
- **`l10n_ca` is required** — the ledger tests need a Canadian CoA + active CAD + 13% HST.
- A **prod clone is the wrong base** — its existing rows collide with fixed-code test fixtures
(`nexacloud` service / `cpu_seconds` metric) across 5 test files.
- odoo.conf sets `log_level=warn`, so **passing tests log nothing** — exit 0 alone does NOT
prove tests ran (a tag matching zero tests is also exit 0). Confirm execution with
`--log-handler=odoo.addons.fusion_centralize_billing.tests:INFO` (look for `Starting
<Class>.<method>`). The **exit code is authoritative** (1 on any failure).
- Do **NOT** pass `--workers=0` (blanks captured stdout) or `--logfile=/dev/stdout` (errors out).

File diff suppressed because it is too large Load Diff

View File

@@ -1,488 +0,0 @@
// =====================================================================
// Fusion Task Map View - Sidebar + Google Maps
// Theme-aware: uses Odoo/Bootstrap variables for dark mode support
// =====================================================================
$sidebar-width: 340px;
$transition-speed: .25s;
.o_fusion_task_map_view {
height: 100%;
.o_content {
height: 100%;
display: flex;
flex-direction: column;
}
}
// ── Main wrapper: sidebar + map side by side ────────────────────────
.fc_map_wrapper {
display: flex;
flex-direction: row;
height: 100%;
min-height: 0;
overflow: hidden;
position: relative;
}
// ── Sidebar ─────────────────────────────────────────────────────────
.fc_sidebar {
width: $sidebar-width;
min-width: $sidebar-width;
max-width: $sidebar-width;
background: var(--o-view-background-color, $o-view-background-color);
border-right: 1px solid $border-color;
display: flex;
flex-direction: column;
transition: width $transition-speed ease, min-width $transition-speed ease,
max-width $transition-speed ease, opacity $transition-speed ease;
overflow: hidden;
&--collapsed {
width: 0;
min-width: 0;
max-width: 0;
opacity: 0;
border-right: none;
}
}
.fc_sidebar_header {
padding: 14px 16px 12px;
border-bottom: 1px solid $border-color;
flex-shrink: 0;
h6 {
font-size: 14px;
color: $headings-color;
}
}
.fc_sidebar_body {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 0;
&::-webkit-scrollbar { width: 5px; }
&::-webkit-scrollbar-track { background: transparent; }
&::-webkit-scrollbar-thumb { background: $border-color; border-radius: 4px; }
}
.fc_sidebar_footer {
padding: 10px 16px;
border-top: 1px solid $border-color;
flex-shrink: 0;
}
.fc_sidebar_empty {
text-align: center;
padding: 40px 20px;
color: $text-muted;
}
// ── Day filter chips ────────────────────────────────────────────────
.fc_day_filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.fc_day_chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 10px;
font-size: 11px;
font-weight: 600;
border: 1px solid $border-color;
border-radius: 12px;
background: transparent;
color: $text-muted;
cursor: pointer;
transition: all .15s;
line-height: 18px;
&:hover {
border-color: rgba($primary, .3);
color: $body-color;
}
&--active {
color: #fff !important;
border-color: transparent !important;
}
&--all {
color: $body-color;
font-weight: 500;
&:hover { background: rgba($primary, .1); }
}
}
.fc_day_chip_count {
font-size: 10px;
opacity: .8;
}
.fc_group_hidden_tag {
font-size: 9px;
text-transform: uppercase;
letter-spacing: .5px;
color: $text-muted;
background: rgba($secondary, .1);
padding: 0 5px;
border-radius: 3px;
margin-left: 4px;
font-weight: 500;
}
// ── Technician filter chips ─────────────────────────────────────────
.fc_tech_filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.fc_tech_chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px 3px 4px;
font-size: 11px;
font-weight: 600;
border: 1px solid $border-color;
border-radius: 14px;
background: transparent;
color: $text-muted;
cursor: pointer;
transition: all .15s;
line-height: 18px;
max-width: 100%;
overflow: hidden;
&:hover {
border-color: rgba($primary, .35);
color: $body-color;
background: rgba($primary, .06);
}
&--active {
background: $primary !important;
color: #fff !important;
border-color: $primary !important;
.fc_tech_chip_avatar {
background: rgba(#fff, .25);
color: #fff;
}
}
&--all {
padding: 3px 10px;
color: $body-color;
font-weight: 500;
&:hover { background: rgba($primary, .1); }
}
}
.fc_tech_chip_avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba($secondary, .15);
color: $body-color;
font-size: 9px;
font-weight: 700;
flex-shrink: 0;
}
.fc_tech_chip_name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Collapsed toggle button (floating)
.fc_sidebar_toggle_btn {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: 15;
background: var(--o-view-background-color, $o-view-background-color);
border: 1px solid $border-color;
border-left: none;
border-radius: 0 8px 8px 0;
padding: 12px 6px;
cursor: pointer;
box-shadow: 2px 0 6px rgba(0,0,0,.08);
color: $text-muted;
transition: background .15s;
&:hover {
background: $o-gray-100;
color: $body-color;
}
}
// ── Group headers ───────────────────────────────────────────────────
.fc_group_header {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
user-select: none;
font-weight: 600;
font-size: 12px;
color: $text-muted;
text-transform: uppercase;
letter-spacing: .5px;
background: rgba($secondary, .08);
border-bottom: 1px solid $border-color;
transition: background .15s;
&:hover {
background: rgba($secondary, .15);
}
.fa-caret-right,
.fa-caret-down {
width: 14px;
text-align: center;
font-size: 13px;
}
}
.fc_group_label {
flex: 1;
}
.fc_group_badge {
background: rgba($secondary, .2);
color: $body-color;
font-size: 10px;
font-weight: 700;
padding: 1px 7px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
// ── Task cards ──────────────────────────────────────────────────────
.fc_group_tasks {
padding: 4px 0;
}
.fc_task_card {
margin: 3px 10px;
padding: 10px 12px;
background: var(--o-view-background-color, $o-view-background-color);
border: 1px solid $border-color;
border-radius: 8px;
cursor: pointer;
transition: all .15s;
position: relative;
&:hover {
background: rgba($primary, .05);
border-color: rgba($primary, .2);
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
&--active {
background: rgba($primary, .1) !important;
border-color: rgba($primary, .35) !important;
box-shadow: 0 0 0 2px rgba($primary, .15);
}
}
.fc_task_card_top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.fc_task_num {
display: inline-block;
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 1px 8px;
border-radius: 4px;
line-height: 18px;
}
.fc_task_status {
font-size: 11px;
font-weight: 600;
}
.fc_task_client {
font-size: 13px;
font-weight: 600;
color: $headings-color;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fc_task_meta {
display: flex;
gap: 12px;
font-size: 11px;
color: $body-color;
margin-bottom: 3px;
.fa { opacity: .5; }
}
.fc_task_date {
font-size: 11px;
color: #6366f1;
font-weight: 600;
margin-bottom: 3px;
.fa { opacity: .5; }
}
.fc_task_detail {
font-size: 11px;
color: $body-color;
margin-bottom: 2px;
.fa { opacity: .5; }
}
.fc_task_address {
font-size: 10px;
color: $text-muted;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
.fc_task_bottom_row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
flex-wrap: wrap;
}
.fc_task_travel {
display: inline-flex;
align-items: center;
font-size: 10px;
color: $body-color;
background: rgba($secondary, .1);
padding: 1px 8px;
border-radius: 4px;
.fa { opacity: .5; }
}
.fc_task_source {
display: inline-flex;
align-items: center;
font-size: 10px;
color: #fff;
font-weight: 600;
padding: 1px 8px;
border-radius: 4px;
.fa { opacity: .8; }
}
.fc_task_edit_btn {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 600;
color: var(--btn-primary-color, #fff);
background: var(--btn-primary-bg, #{$primary});
padding: 2px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: auto;
transition: all .15s;
&:hover {
opacity: .85;
filter: brightness(1.15);
}
}
// ── Map area ────────────────────────────────────────────────────────
.fc_map_area {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-width: 0;
position: relative;
}
.fc_map_legend_bar {
flex: 0 0 auto;
font-size: 12px;
min-height: 40px;
}
.fc_map_container {
flex: 1 1 auto;
position: relative;
min-height: 400px;
}
// ── Google Maps InfoWindow override ──────────────────────────────────
.gm-style-iw-d {
overflow: auto !important;
}
.gm-style .gm-style-iw-c {
padding: 0 !important;
border-radius: 10px !important;
overflow: hidden !important;
box-shadow: 0 4px 20px rgba(0,0,0,.15) !important;
}
.gm-style .gm-style-iw-tc {
display: none !important;
}
.gm-style .gm-ui-hover-effect {
display: none !important;
}
// ── Responsive ──────────────────────────────────────────────────────
@media (max-width: 768px) {
.fc_map_wrapper {
flex-direction: column;
}
.fc_sidebar {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
max-height: 40vh;
border-right: none;
border-bottom: 1px solid $border-color;
&--collapsed {
max-height: 0;
opacity: 0;
}
}
.fc_sidebar_toggle_btn {
top: auto;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
border-radius: 8px;
border: 1px solid $border-color;
padding: 8px 16px;
}
.fc_map_area {
flex: 1;
min-height: 300px;
}
}

View File

@@ -1,255 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_tasks.FusionTaskMapView">
<div class="o_fusion_task_map_view">
<Layout display="display">
<t t-set-slot="control-panel-additional-actions">
<CogMenu/>
</t>
<t t-set-slot="layout-buttons">
<t t-call="{{ props.buttonTemplate }}"/>
</t>
<t t-set-slot="layout-actions">
<SearchBar toggler="searchBarToggler"/>
</t>
<t t-set-slot="control-panel-navigation-additional">
<t t-component="searchBarToggler.component" t-props="searchBarToggler.props"/>
</t>
<div class="fc_map_wrapper">
<!-- ========== SIDEBAR ========== -->
<div t-att-class="'fc_sidebar' + (state.sidebarOpen ? '' : ' fc_sidebar--collapsed')">
<!-- Sidebar header -->
<div class="fc_sidebar_header">
<div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-bold">
<i class="fa fa-list-ul me-2"/>Deliveries
<span class="badge text-bg-primary ms-1" t-esc="state.taskCount"/>
</h6>
<button class="btn btn-sm btn-link text-muted p-0" t-on-click="toggleSidebar"
title="Toggle sidebar">
<i t-att-class="'fa ' + (state.sidebarOpen ? 'fa-chevron-left' : 'fa-chevron-right')"/>
</button>
</div>
<!-- New task button -->
<button class="btn btn-primary btn-sm w-100 mt-2" t-on-click="createNewTask">
<i class="fa fa-plus me-1"/>New Delivery Task
</button>
<!-- Day filter chips -->
<div class="fc_day_filters mt-2">
<t t-foreach="state.groups" t-as="group" t-key="group.key + '_filter'">
<button t-att-class="'fc_day_chip' + (isGroupVisible(group.key) ? ' fc_day_chip--active' : '')"
t-att-style="isGroupVisible(group.key) ? 'background:' + group.dayColor + ';color:#fff;border-color:' + group.dayColor : ''"
t-on-click="() => this.toggleDayFilter(group.key)">
<t t-esc="group.label"/>
<span class="fc_day_chip_count" t-esc="group.count"/>
</button>
</t>
<button class="fc_day_chip fc_day_chip--all" t-on-click="showAllDays"
title="Show all">All</button>
</div>
<!-- Technician filter -->
<t t-if="state.allTechnicians.length > 1">
<div class="fc_tech_filters mt-2">
<t t-foreach="state.allTechnicians" t-as="tech" t-key="tech.id">
<button t-att-class="'fc_tech_chip' + (isTechVisible(tech.id) ? ' fc_tech_chip--active' : '')"
t-on-click="() => this.toggleTechFilter(tech.id)"
t-att-title="tech.name">
<span class="fc_tech_chip_avatar" t-esc="tech.initials"/>
<span class="fc_tech_chip_name" t-esc="tech.name"/>
</button>
</t>
<button class="fc_tech_chip fc_tech_chip--all" t-on-click="showAllTechs"
title="Show all technicians">All</button>
</div>
</t>
</div>
<!-- Sidebar body: grouped task list -->
<div class="fc_sidebar_body">
<t t-foreach="state.groups" t-as="group" t-key="group.key">
<!-- Group header (collapsible) with day color -->
<div class="fc_group_header" t-on-click="() => this.toggleGroup(group.key)">
<i t-att-class="'fa me-1 ' + (isGroupCollapsed(group.key) ? 'fa-caret-right' : 'fa-caret-down')"/>
<i class="fa fa-circle me-1" style="font-size:8px;"
t-att-style="'color:' + group.dayColor"/>
<span class="fc_group_label" t-esc="group.label"/>
<span t-if="!isGroupVisible(group.key)" class="fc_group_hidden_tag">hidden</span>
<span class="fc_group_badge" t-esc="group.count"/>
</div>
<!-- Group tasks -->
<div t-if="!isGroupCollapsed(group.key)" class="fc_group_tasks">
<t t-foreach="group.tasks" t-as="task" t-key="task.id">
<div t-att-class="'fc_task_card' + (state.activeTaskId === task.id ? ' fc_task_card--active' : '')"
t-on-click="() => this.focusTask(task.id)">
<!-- Card top row: number + status -->
<div class="fc_task_card_top">
<span class="fc_task_num" t-att-style="'background:' + task._dayColor">
<t t-esc="'#' + task._scheduleNum"/>
</span>
<span class="fc_task_status" t-att-style="'color:' + task._statusColor">
<i t-att-class="'fa ' + task._statusIcon" style="margin-right:3px;"/>
<t t-esc="task._statusLabel"/>
</span>
</div>
<!-- Client name -->
<div class="fc_task_client" t-esc="task._clientName"/>
<!-- Type + time -->
<div class="fc_task_meta">
<span><i class="fa fa-tag me-1"/><t t-esc="task._typeLbl"/></span>
<span><i class="fa fa-clock-o me-1"/><t t-esc="task._timeRange"/></span>
</div>
<!-- Date -->
<div class="fc_task_date">
<i class="fa fa-calendar me-1"/><t t-esc="task._dateLabel"/>
</div>
<!-- Technician + address -->
<div class="fc_task_detail">
<span><i class="fa fa-user me-1"/><t t-esc="task._techName"/></span>
</div>
<div t-if="task.address_display" class="fc_task_address">
<i class="fa fa-map-marker me-1"/>
<t t-esc="task.address_display"/>
</div>
<!-- Travel + source -->
<div class="fc_task_bottom_row">
<span t-if="task.travel_time_minutes" class="fc_task_travel">
<i class="fa fa-car me-1"/>
<t t-esc="task.travel_time_minutes"/> min travel
</span>
<span t-if="task._sourceLabel" class="fc_task_source"
t-att-style="'background:' + task._sourceColor">
<i class="fa fa-building-o me-1"/>
<t t-esc="task._sourceLabel"/>
</span>
<span class="fc_task_edit_btn"
t-on-click.stop="() => this.openTask(task.id)"
title="Edit task">
<i class="fa fa-pencil me-1"/>Edit
</span>
</div>
</div>
</t>
</div>
</t>
<!-- Empty state -->
<div t-if="state.groups.length === 0 and !state.loading" class="fc_sidebar_empty">
<i class="fa fa-inbox fa-2x text-muted d-block mb-2"/>
<span class="text-muted">No tasks found</span>
</div>
</div>
<!-- Sidebar footer: technician count -->
<div class="fc_sidebar_footer">
<div class="d-flex align-items-center gap-2">
<svg width="14" height="14" viewBox="0 0 48 48">
<rect x="2" y="2" width="44" height="44" rx="12" ry="12" fill="#1d4ed8" stroke="#fff" stroke-width="3"/>
<text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-family="Arial,sans-serif" font-weight="bold">T</text>
</svg>
<small class="text-muted">
<t t-esc="state.techCount"/> technician(s) online
</small>
</div>
</div>
</div>
<!-- Collapsed sidebar toggle -->
<button t-if="!state.sidebarOpen"
class="fc_sidebar_toggle_btn" t-on-click="toggleSidebar"
title="Open sidebar">
<i class="fa fa-chevron-right"/>
</button>
<!-- ========== MAP AREA ========== -->
<div class="fc_map_area">
<!-- Legend bar -->
<div class="fc_map_legend_bar d-flex align-items-center gap-3 px-3 py-2 border-bottom bg-view flex-wrap">
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showTasks ? 'btn-primary' : 'btn-outline-secondary'"
t-on-click="toggleTasks">
<i class="fa fa-map-marker"/>Tasks <t t-esc="state.taskCount"/>
</button>
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showTechnicians ? 'btn-primary' : 'btn-outline-secondary'"
t-on-click="toggleTechnicians">
<i class="fa fa-user"/>Techs <t t-esc="state.techCount"/>
</button>
<span class="border-start mx-1" style="height:20px;"/>
<span class="text-muted fw-bold" style="font-size:11px;">Pins:</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#f59e0b;"/>Pending</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#ef4444;"/>Today</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#3b82f6;"/>Tomorrow</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#10b981;"/>This Week</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#a855f7;"/>Upcoming</span>
<span style="font-size:11px;"><i class="fa fa-map-marker me-1" style="color:#9ca3af;"/>Yesterday</span>
<span class="flex-grow-1"/>
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showRoute ? 'btn-info' : 'btn-outline-secondary'"
t-on-click="toggleRoute" title="Toggle route animation">
<i class="fa fa-road"/>Route
</button>
<button class="btn btn-sm d-flex align-items-center gap-1"
t-att-class="state.showTraffic ? 'btn-warning' : 'btn-outline-secondary'"
t-on-click="toggleTraffic" title="Toggle traffic layer">
<i class="fa fa-car"/>Traffic
</button>
<button class="btn btn-outline-secondary btn-sm" t-on-click="onRefresh" title="Refresh">
<i class="fa fa-refresh" t-att-class="{'fa-spin': state.loading}"/>
</button>
</div>
<!-- Map container -->
<div class="fc_map_container">
<div t-ref="mapContainer" style="position:absolute;top:0;left:0;right:0;bottom:0;"/>
<!-- Loading -->
<div t-if="state.loading"
class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
style="z-index:10;background:rgba(255,255,255,.92);">
<div class="text-center">
<i class="fa fa-spinner fa-spin fa-3x text-primary mb-3 d-block"/>
<span class="text-muted">Loading Google Maps...</span>
</div>
</div>
<!-- Error -->
<div t-if="state.error"
class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
style="z-index:10;background:rgba(255,255,255,.92);">
<div class="alert alert-danger m-4" role="alert">
<i class="fa fa-exclamation-triangle me-2"/><t t-esc="state.error"/>
</div>
</div>
<!-- Empty -->
<div t-if="!state.loading and !state.error and state.taskCount === 0 and state.techCount === 0"
class="position-absolute top-50 start-50 translate-middle text-center" style="z-index:5;">
<div class="bg-white rounded-3 shadow p-4">
<i class="fa fa-map-marker fa-3x text-muted mb-3 d-block"/>
<h5>No locations to show</h5>
<p class="text-muted mb-0">Try adjusting the filters or date range.</p>
</div>
</div>
</div>
</div>
</div>
</Layout>
</div>
</t>
<t t-name="fusion_tasks.FusionTaskMapView.Buttons"/>
</templates>

View File

@@ -1,355 +0,0 @@
# Graph Report - /Users/gurpreet/Github/Odoo-Modules/Entech Plating (2026-04-22)
## Corpus Check
- 13 files · ~19,911 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 312 nodes · 494 edges · 45 communities detected
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 20 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]]
## God Nodes (most connected - your core abstractions)
1. `FusionTechnicianTask` - 65 edges
2. `FusionTaskMapController` - 38 edges
3. `FusionTaskSyncConfig` - 14 edges
4. `create()` - 10 edges
5. `_float_to_time_str()` - 10 edges
6. `FusionEmailBuilderMixin` - 9 edges
7. `create()` - 7 edges
8. `_check_no_overlap()` - 6 edges
9. `_get_tech_start_locations()` - 6 edges
10. `_cron_check_late_arrivals()` - 6 edges
## Surprising Connections (you probably didn't know these)
- `log_location()` --calls--> `create()` [INFERRED]
/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/technician_location.py → /Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_partner.py
- `create()` --calls--> `_push_tasks()` [INFERRED]
/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/technician_task.py → /Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/task_sync.py
- `get_map_data()` --calls--> `get_latest_locations()` [INFERRED]
/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/technician_task.py → /Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/technician_location.py
- `register_subscription()` --calls--> `create()` [INFERRED]
/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py → /Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_partner.py
## Communities
### Community 0 - "Community 0"
Cohesion: 0.05
Nodes (28): FusionTechnicianTask, _onchange_is_in_store(), Populate address fields from a partner record., Create or update a linked calendar.event for external calendar sync. On, Hook: post task creation notice to linked order chatter. Override in fus, Hook: mark linked sale orders as ready for delivery. Override in fusion_, Mark task as Completed., Hook: check additional requirements before task completion. Override in (+20 more)
### Community 1 - "Community 1"
Cohesion: 0.07
Nodes (12): classifyDate(), classifyTask(), extractTechnicians(), floatToTime12(), FusionMapArchParser, FusionTaskMapController, groupTasks(), initialsOf() (+4 more)
### Community 2 - "Community 2"
Cohesion: 0.09
Nodes (22): FusionPushSubscription, register_subscription(), create(), ResPartner, _cron_pull_remote_tasks(), FusionTaskSyncConfig, _push_shadow_status(), _push_tasks() (+14 more)
### Community 3 - "Community 3"
Cohesion: 0.09
Nodes (18): _check_no_overlap(), _compute_datetimes(), _compute_prev_task_summary(), _compute_schedule_info(), _compute_time_displays(), default_get(), _float_to_time_str(), _onchange_compute_end_time() (+10 more)
### Community 4 - "Community 4"
Cohesion: 0.11
Nodes (16): _cron_calculate_travel_times(), _cron_send_push_notifications(), _get_clock_in_locations(), _get_tech_start_locations(), Get the start address for a technician. Priority: 1. Technici, Geocode an address string and return (lat, lng) or (0.0, 0.0)., Recalculate travel for a set of (tech_id, date) combinations. Start-poi, Get the technician's most recent GPS location. Priority: 1. L (+8 more)
### Community 5 - "Community 5"
Cohesion: 0.11
Nodes (12): _cron_check_late_arrivals(), Recalculate travel time for THIS task from the tech's current GPS. Call, Check that all earlier tasks for the same technician+date are completed., Write GPS coordinates from context onto the task record., Mark task as En Route., Mark task as In Progress., Cancel the task. Sends cancellation email and runs cancel hooks., Hook: additional side-effects after task cancellation. Override in fusio (+4 more)
### Community 6 - "Community 6"
Cohesion: 0.14
Nodes (9): FusionEmailBuilderMixin, Build a labeled details table section. Args: heading: Secti, Build a left-border accent note block., Build a centered CTA button., Build a dashed-border attachment callout. Args: description, Return an inline status badge/pill HTML snippet., Return company name, phone, email for email templates., Check if email notifications are enabled in settings. (+1 more)
### Community 7 - "Community 7"
Cohesion: 0.12
Nodes (10): FusionTechnicianLocation, get_latest_locations(), log_location(), create(), _fill_address_vals(), get_map_data(), Hook: fill address from linked records during create. Base implementati, Hook: post-create side-effects for linked records. Override in fusion_c (+2 more)
### Community 8 - "Community 8"
Cohesion: 1.0
Nodes (2): _fusion_tasks_post_init(), Post-install hook for fusion_tasks. 1. Sets default ICP values (upsert - sa
### Community 9 - "Community 9"
Cohesion: 1.0
Nodes (1): ResCompany
### Community 10 - "Community 10"
Cohesion: 1.0
Nodes (1): ResUsers
### Community 11 - "Community 11"
Cohesion: 1.0
Nodes (1): ResConfigSettings
### Community 12 - "Community 12"
Cohesion: 1.0
Nodes (0):
### Community 13 - "Community 13"
Cohesion: 1.0
Nodes (0):
### Community 14 - "Community 14"
Cohesion: 1.0
Nodes (1): Log the current user's location. Called from portal JS.
### Community 15 - "Community 15"
Cohesion: 1.0
Nodes (1): Get the most recent location for each technician (for map view). Includ
### Community 16 - "Community 16"
Cohesion: 1.0
Nodes (1): Remove location logs based on configurable retention setting. Setting (
### Community 17 - "Community 17"
Cohesion: 1.0
Nodes (1): Register or update a push subscription.
### Community 18 - "Community 18"
Cohesion: 1.0
Nodes (1): Generate 12-hour time slots every 15 minutes, store hours only (9 AM - 6 PM).
### Community 19 - "Community 19"
Cohesion: 1.0
Nodes (1): Sync the 12h selection fields from the raw float values.
### Community 20 - "Community 20"
Cohesion: 1.0
Nodes (1): Convert float hours to readable time strings.
### Community 21 - "Community 21"
Cohesion: 1.0
Nodes (1): Set default duration based on task type.
### Community 22 - "Community 22"
Cohesion: 1.0
Nodes (1): Auto-compute end time from start + duration. Also run overlap check.
### Community 23 - "Community 23"
Cohesion: 1.0
Nodes (1): Combine date + float time into proper Datetime fields for calendar. time
### Community 24 - "Community 24"
Cohesion: 1.0
Nodes (1): Show booked + available time slots for the technician on the selected date.
### Community 25 - "Community 25"
Cohesion: 1.0
Nodes (1): Show previous task info + travel time warning with color coding.
### Community 26 - "Community 26"
Cohesion: 1.0
Nodes (1): Auto-fill company address when task is marked as in-store.
### Community 27 - "Community 27"
Cohesion: 1.0
Nodes (1): Auto-fill address fields from the selected client's address.
### Community 28 - "Community 28"
Cohesion: 1.0
Nodes (1): Non-in-store tasks must have a geocoded address.
### Community 29 - "Community 29"
Cohesion: 1.0
Nodes (1): Prevent overlapping bookings for the same technician on the same date.
### Community 30 - "Community 30"
Cohesion: 1.0
Nodes (1): Auto-set start/end time to the first available slot when tech+date change.
### Community 31 - "Community 31"
Cohesion: 1.0
Nodes (1): Handle calendar time range selection: pre-fill date + times from context.
### Community 32 - "Community 32"
Cohesion: 1.0
Nodes (1): Helper to fill address vals dict from a partner record.
### Community 33 - "Community 33"
Cohesion: 1.0
Nodes (1): Return task data, technician locations, and Google Maps API key. Args:
### Community 34 - "Community 34"
Cohesion: 1.0
Nodes (1): Build a dict of technician start locations for route origins. Priority
### Community 35 - "Community 35"
Cohesion: 1.0
Nodes (1): Get today's clock-in lat/lng from fusion_clock if installed. Uses the t
### Community 36 - "Community 36"
Cohesion: 1.0
Nodes (1): Cron job: Calculate travel times for today and tomorrow. Runs every 15
### Community 37 - "Community 37"
Cohesion: 1.0
Nodes (1): Cron: detect tasks where the technician hasn't started and the scheduled
### Community 38 - "Community 38"
Cohesion: 1.0
Nodes (1): Cron: Send push notifications for upcoming tasks.
### Community 39 - "Community 39"
Cohesion: 1.0
Nodes (1): Convert float hours to time string like '9:30 AM'.
### Community 40 - "Community 40"
Cohesion: 1.0
Nodes (1): Push local task changes to all active remote instances. Called from tech
### Community 41 - "Community 41"
Cohesion: 1.0
Nodes (1): Push local status changes on shadow tasks back to their source instance.
### Community 42 - "Community 42"
Cohesion: 1.0
Nodes (1): Push a technician's location update to all remote instances. Called whe
### Community 43 - "Community 43"
Cohesion: 1.0
Nodes (1): Cron job: pull tasks and technician locations from all active remote instances.
### Community 44 - "Community 44"
Cohesion: 1.0
Nodes (1): Remove shadow tasks older than 30 days (completed/cancelled).
## Knowledge Gaps
- **119 isolated node(s):** `Post-install hook for fusion_tasks. 1. Sets default ICP values (upsert - sa`, `FusionTechnicianLocation`, `Log the current user's location. Called from portal JS.`, `Get the most recent location for each technician (for map view). Includ`, `Remove location logs based on configurable retention setting. Setting (` (+114 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **Thin community `Community 9`** (2 nodes): `ResCompany`, `res_company.py`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 10`** (2 nodes): `ResUsers`, `res_users.py`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 11`** (2 nodes): `ResConfigSettings`, `res_config_settings.py`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 12`** (1 nodes): `__init__.py`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 13`** (1 nodes): `__manifest__.py`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 14`** (1 nodes): `Log the current user's location. Called from portal JS.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 15`** (1 nodes): `Get the most recent location for each technician (for map view). Includ`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 16`** (1 nodes): `Remove location logs based on configurable retention setting. Setting (`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 17`** (1 nodes): `Register or update a push subscription.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 18`** (1 nodes): `Generate 12-hour time slots every 15 minutes, store hours only (9 AM - 6 PM).`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 19`** (1 nodes): `Sync the 12h selection fields from the raw float values.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 20`** (1 nodes): `Convert float hours to readable time strings.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 21`** (1 nodes): `Set default duration based on task type.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 22`** (1 nodes): `Auto-compute end time from start + duration. Also run overlap check.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 23`** (1 nodes): `Combine date + float time into proper Datetime fields for calendar. time`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 24`** (1 nodes): `Show booked + available time slots for the technician on the selected date.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 25`** (1 nodes): `Show previous task info + travel time warning with color coding.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 26`** (1 nodes): `Auto-fill company address when task is marked as in-store.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 27`** (1 nodes): `Auto-fill address fields from the selected client's address.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 28`** (1 nodes): `Non-in-store tasks must have a geocoded address.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 29`** (1 nodes): `Prevent overlapping bookings for the same technician on the same date.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 30`** (1 nodes): `Auto-set start/end time to the first available slot when tech+date change.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 31`** (1 nodes): `Handle calendar time range selection: pre-fill date + times from context.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 32`** (1 nodes): `Helper to fill address vals dict from a partner record.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 33`** (1 nodes): `Return task data, technician locations, and Google Maps API key. Args:`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 34`** (1 nodes): `Build a dict of technician start locations for route origins. Priority`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 35`** (1 nodes): `Get today's clock-in lat/lng from fusion_clock if installed. Uses the t`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 36`** (1 nodes): `Cron job: Calculate travel times for today and tomorrow. Runs every 15`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 37`** (1 nodes): `Cron: detect tasks where the technician hasn't started and the scheduled`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 38`** (1 nodes): `Cron: Send push notifications for upcoming tasks.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 39`** (1 nodes): `Convert float hours to time string like '9:30 AM'.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 40`** (1 nodes): `Push local task changes to all active remote instances. Called from tech`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 41`** (1 nodes): `Push local status changes on shadow tasks back to their source instance.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 42`** (1 nodes): `Push a technician's location update to all remote instances. Called whe`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 43`** (1 nodes): `Cron job: pull tasks and technician locations from all active remote instances.`
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
- **Thin community `Community 44`** (1 nodes): `Remove shadow tasks older than 30 days (completed/cancelled).`
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 `FusionTechnicianTask` connect `Community 0` to `Community 3`, `Community 4`, `Community 5`, `Community 7`?**
_High betweenness centrality (0.331) - this node is a cross-community bridge._
- **Why does `_push_shadow_status()` connect `Community 2` to `Community 5`?**
_High betweenness centrality (0.031) - this node is a cross-community bridge._
- **Why does `_push_tasks()` connect `Community 2` to `Community 5`, `Community 7`?**
_High betweenness centrality (0.027) - this node is a cross-community bridge._
- **What connects `Post-install hook for fusion_tasks. 1. Sets default ICP values (upsert - sa`, `FusionTechnicianLocation`, `Log the current user's location. Called from portal JS.` to the rest of the system?**
_119 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
_Cohesion score 0.07 - nodes in this community are weakly interconnected._
- **Should `Community 2` be split into smaller, more focused modules?**
_Cohesion score 0.09 - nodes in this community are weakly interconnected._

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L9", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L10", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L11", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L12", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/__init__.py", "source_location": "L13", "weight": 1.0}], "raw_calls": []}

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_company_py", "label": "res_company.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_company.py", "source_location": "L1"}, {"id": "res_company_rescompany", "label": "ResCompany", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_company.py", "source_location": "L8"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_company_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_company.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_company_py", "target": "res_company_rescompany", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_company.py", "source_location": "L8", "weight": 1.0}], "raw_calls": []}

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_config_settings_py", "label": "res_config_settings.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_config_settings.py", "source_location": "L1"}, {"id": "res_config_settings_resconfigsettings", "label": "ResConfigSettings", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_config_settings.py", "source_location": "L8"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_config_settings_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_config_settings.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_config_settings_py", "target": "res_config_settings_resconfigsettings", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_config_settings.py", "source_location": "L8", "weight": 1.0}], "raw_calls": []}

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_users_py", "label": "res_users.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_users.py", "source_location": "L1"}, {"id": "res_users_resusers", "label": "ResUsers", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_users.py", "source_location": "L8"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_users_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_users.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_res_users_py", "target": "res_users_resusers", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/res_users.py", "source_location": "L8", "weight": 1.0}], "raw_calls": []}

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L1"}, {"id": "init_fusion_tasks_post_init", "label": "_fusion_tasks_post_init()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L8"}, {"id": "init_rationale_9", "label": "Post-install hook for fusion_tasks. 1. Sets default ICP values (upsert - sa", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L9"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_init_py", "target": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_init_py", "target": "init_fusion_tasks_post_init", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L8", "weight": 1.0}, {"source": "init_rationale_9", "target": "init_fusion_tasks_post_init", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L9", "weight": 1.0}], "raw_calls": [{"caller_nid": "init_fusion_tasks_post_init", "callee": "sudo", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L15"}, {"caller_nid": "init_fusion_tasks_post_init", "callee": "items", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L25"}, {"caller_nid": "init_fusion_tasks_post_init", "callee": "get_param", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L26"}, {"caller_nid": "init_fusion_tasks_post_init", "callee": "set_param", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L27"}, {"caller_nid": "init_fusion_tasks_post_init", "callee": "ref", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L30"}, {"caller_nid": "init_fusion_tasks_post_init", "callee": "search", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L32"}, {"caller_nid": "init_fusion_tasks_post_init", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/__init__.py", "source_location": "L36"}]}

View File

@@ -1 +0,0 @@
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_push_subscription_py", "label": "push_subscription.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L1"}, {"id": "push_subscription_fusionpushsubscription", "label": "FusionPushSubscription", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L15"}, {"id": "push_subscription_register_subscription", "label": "register_subscription()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L55"}, {"id": "push_subscription_rationale_56", "label": "Register or update a push subscription.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L56"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_push_subscription_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L9", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_push_subscription_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L10", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_push_subscription_py", "target": "push_subscription_fusionpushsubscription", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L15", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_entech_plating_fusion_tasks_models_push_subscription_py", "target": "push_subscription_register_subscription", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L55", "weight": 1.0}, {"source": "push_subscription_rationale_56", "target": "push_subscription_fusionpushsubscription_register_subscription", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L56", "weight": 1.0}], "raw_calls": [{"caller_nid": "push_subscription_register_subscription", "callee": "search", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L57"}, {"caller_nid": "push_subscription_register_subscription", "callee": "sudo", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L57"}, {"caller_nid": "push_subscription_register_subscription", "callee": "write", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L59"}, {"caller_nid": "push_subscription_register_subscription", "callee": "create", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L67"}, {"caller_nid": "push_subscription_register_subscription", "callee": "sudo", "source_file": "/Users/gurpreet/Github/Odoo-Modules/Entech Plating/fusion_tasks/models/push_subscription.py", "source_location": "L67"}]}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More