Compare commits
14 Commits
main
...
d7bbeb49b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7bbeb49b7 | ||
|
|
2737bc481c | ||
|
|
0e595e6129 | ||
|
|
a0f783ab14 | ||
|
|
82a13b2ce5 | ||
|
|
0230670bdc | ||
|
|
86e89ca419 | ||
|
|
749c0335fa | ||
|
|
092423d7de | ||
|
|
9c52fac9ba | ||
|
|
d2f8934a53 | ||
|
|
113427f7e2 | ||
|
|
3559eb1fd5 | ||
|
|
9f28dce160 |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"WebFetch(domain:docs.clover.com)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"UserPromptSubmit": [],
|
|
||||||
"Stop": [],
|
|
||||||
"Notification": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
.gitignore
vendored
70
.gitignore
vendored
@@ -1,70 +0,0 @@
|
|||||||
# Python bytecode
|
|
||||||
__pycache__/
|
|
||||||
*.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/
|
|
||||||
@@ -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
|
|
||||||
@@ -77,7 +77,6 @@ Odoo content-hashes the compiled bundle URL (`/web/assets/<hash>/...`). When CSS
|
|||||||
|
|
||||||
## Cursor-Managed Modules
|
## 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_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 1–11 shipped at `19.0.2.2.4`; not production-deployed)
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
- Local dev: `docker exec odoo-dev-app odoo -d fusion-dev -u <module> --stop-after-init`
|
- Local dev: `docker exec odoo-dev-app odoo -d fusion-dev -u <module> --stop-after-init`
|
||||||
|
|||||||
210
CLAUDE.md
210
CLAUDE.md
@@ -12,30 +12,9 @@
|
|||||||
3. **Backend OWL**: Use standalone `rpc()` from `@web/core/network/rpc`. NOT `useService("rpc")`. `static props = []` not `{}`.
|
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).
|
4. **HTTP routes**: `type="jsonrpc"` — NOT `type="json"` (deprecated).
|
||||||
5. **res.config.settings**: Only boolean/integer/float/char/selection/many2one/datetime. NO Date fields.
|
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.
|
||||||
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.
|
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.
|
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
|
## 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:
|
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:
|
||||||
@@ -96,41 +75,14 @@ Odoo content-hashes the compiled bundle URL (`/web/assets/<hash>/...`). When CSS
|
|||||||
- Canadian English for all user-facing text
|
- Canadian English for all user-facing text
|
||||||
- Currency: `$` sign with Monetary fields + currency_id
|
- Currency: `$` sign with Monetary fields + currency_id
|
||||||
|
|
||||||
## Module-Specific Notes
|
## Cursor-Managed Modules
|
||||||
- **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_clock** is currently being modified in Cursor — always read files fresh before editing, don't assume you know the current state
|
||||||
- **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
|
## Workflow
|
||||||
- Local dev: `docker exec odoo-modsdev-app odoo -d fusion-dev -u <module> --stop-after-init`
|
- Local dev: `docker exec odoo-dev-app odoo -d fusion-dev -u <module> --stop-after-init`
|
||||||
- Local URL: http://localhost:8082
|
- Local URL: http://localhost:8069
|
||||||
- **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.
|
- 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
|
## Supabase Knowledge Base
|
||||||
Before starting unfamiliar work, check Supabase for context:
|
Before starting unfamiliar work, check Supabase for context:
|
||||||
```bash
|
```bash
|
||||||
@@ -140,155 +92,3 @@ PGPASSWORD='a09e12e0995dc29446631fa458f3d4b3' psql -h 100.74.28.73 -p 5433 -U po
|
|||||||
- `fusionapps.issues` — known issues and fixes
|
- `fusionapps.issues` — known issues and fixes
|
||||||
- `fusionapps.code_snippets` — reference code
|
- `fusionapps.code_snippets` — reference code
|
||||||
- `fusionapps.quick_commands` — deployment and admin commands
|
- `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).
|
|
||||||
|
|||||||
58
SYNC.md
58
SYNC.md
@@ -1,58 +0,0 @@
|
|||||||
# Syncing Odoo-Modules across machines (Mac + Windows)
|
|
||||||
|
|
||||||
Each module/suite folder here is its **own git repo** (private on GitHub at
|
|
||||||
`gsinghpal/<name>`, mirrored to gitea `admin/<name>`). This parent folder is a
|
|
||||||
separate repo that holds the shared files (CLAUDE.md, docs, scripts, these sync
|
|
||||||
helpers). The cloud (GitHub) is the hub: both machines push to it and pull from it.
|
|
||||||
|
|
||||||
Nothing here ever deletes your work. Pulls are fast-forward only, so local changes
|
|
||||||
are never overwritten; pushes only send commits.
|
|
||||||
|
|
||||||
## First-time setup on a new machine (e.g. the Windows PC)
|
|
||||||
|
|
||||||
1. Install **Git** (Git for Windows includes "Git Bash", which runs these scripts).
|
|
||||||
2. Sign in to GitHub once so git can push/pull:
|
|
||||||
- easiest: `gh auth login` (or let Git Credential Manager prompt on first pull)
|
|
||||||
3. Get everything:
|
|
||||||
```
|
|
||||||
git clone https://github.com/gsinghpal/Odoo-Modules.git
|
|
||||||
cd Odoo-Modules
|
|
||||||
bash sync-clone-all.sh
|
|
||||||
```
|
|
||||||
That clones the parent, then all 49 module repos into place.
|
|
||||||
|
|
||||||
(gitea is an optional second mirror. The first push to it will ask for your
|
|
||||||
`git.nexasystems.ca` login. If you only use GitHub, those gitea lines just fail
|
|
||||||
quietly and GitHub stays the source of truth.)
|
|
||||||
|
|
||||||
## Daily workflow (same on Mac and Windows)
|
|
||||||
|
|
||||||
- **Before you start:** `bash sync-pull-all.sh` - pulls the latest for the parent
|
|
||||||
and every module. Anything with local changes or a diverged history is skipped and
|
|
||||||
listed, so you can handle it yourself.
|
|
||||||
- **Do your work**, then **commit inside the module(s) you changed**:
|
|
||||||
```
|
|
||||||
cd fusion_clock
|
|
||||||
git add -A
|
|
||||||
git commit -m "..."
|
|
||||||
cd ..
|
|
||||||
```
|
|
||||||
- **When done:** `bash sync-push-all.sh` - pushes every committed change to GitHub
|
|
||||||
+ gitea, and flags any repo that still has uncommitted changes (so nothing is
|
|
||||||
silently left behind).
|
|
||||||
|
|
||||||
## Golden rule for two machines
|
|
||||||
|
|
||||||
Push from the machine you worked on **before** you switch to the other one, and run
|
|
||||||
`sync-pull-all.sh` on the other machine **before** you start. That keeps both in sync
|
|
||||||
and avoids diverged histories.
|
|
||||||
|
|
||||||
## Helper scripts
|
|
||||||
|
|
||||||
| Script | What it does |
|
|
||||||
|--------|--------------|
|
|
||||||
| `sync-clone-all.sh` | Clone any module repo listed in `repos.txt` that isn't here yet. |
|
|
||||||
| `sync-pull-all.sh` | Fast-forward pull the parent + all modules (safe, never clobbers). |
|
|
||||||
| `sync-push-all.sh` | Push committed work for the parent + all modules to GitHub + gitea. |
|
|
||||||
| `sync-refresh-list.sh` | Rebuild `repos.txt` from the repos present here (after adding/removing a module). |
|
|
||||||
| `repos.txt` | The list of module repo names the scripts act on. |
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
'website',
|
'website',
|
||||||
'mail',
|
'mail',
|
||||||
'fusion_claims',
|
'fusion_claims',
|
||||||
'fusion_portal',
|
'fusion_authorizer_portal',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'security/security.xml',
|
'security/security.xml',
|
||||||
|
|||||||
3
disable_iap_calls/__init__.py
Normal file
3
disable_iap_calls/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
||||||
23
disable_iap_calls/__manifest__.py
Normal file
23
disable_iap_calls/__manifest__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Disable IAP Calls',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Tools',
|
||||||
|
'summary': 'Disables all IAP (In-App Purchase) external API calls',
|
||||||
|
'description': """
|
||||||
|
This module completely disables:
|
||||||
|
- IAP service calls to Odoo servers
|
||||||
|
- OCR/Extract API calls
|
||||||
|
- Lead enrichment API calls
|
||||||
|
- Any other external Odoo API communication
|
||||||
|
|
||||||
|
For local development use only.
|
||||||
|
""",
|
||||||
|
'author': 'Development',
|
||||||
|
'depends': ['iap'],
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
||||||
3
disable_iap_calls/disable_iap_calls/__init__.py
Normal file
3
disable_iap_calls/disable_iap_calls/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
||||||
23
disable_iap_calls/disable_iap_calls/__manifest__.py
Normal file
23
disable_iap_calls/disable_iap_calls/__manifest__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Disable IAP Calls',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Tools',
|
||||||
|
'summary': 'Disables all IAP (In-App Purchase) external API calls',
|
||||||
|
'description': """
|
||||||
|
This module completely disables:
|
||||||
|
- IAP service calls to Odoo servers
|
||||||
|
- OCR/Extract API calls
|
||||||
|
- Lead enrichment API calls
|
||||||
|
- Any other external Odoo API communication
|
||||||
|
|
||||||
|
For local development use only.
|
||||||
|
""",
|
||||||
|
'author': 'Development',
|
||||||
|
'depends': ['iap'],
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
||||||
3
disable_iap_calls/disable_iap_calls/models/__init__.py
Normal file
3
disable_iap_calls/disable_iap_calls/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import iap_account
|
||||||
|
|
||||||
20
disable_iap_calls/disable_iap_calls/models/iap_account.py
Normal file
20
disable_iap_calls/disable_iap_calls/models/iap_account.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Disable all IAP external API calls for local development
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IapAccountDisabled(models.Model):
|
||||||
|
_inherit = 'iap.account'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_credits(self, service_name):
|
||||||
|
"""
|
||||||
|
DISABLED: Return fake unlimited credits
|
||||||
|
"""
|
||||||
|
_logger.info("IAP get_credits DISABLED - returning unlimited credits for %s", service_name)
|
||||||
|
return 999999
|
||||||
|
|
||||||
86
disable_iap_calls/graphify-out/GRAPH_REPORT.md
Normal file
86
disable_iap_calls/graphify-out/GRAPH_REPORT.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Graph Report - /Users/gurpreet/Github/Odoo-Modules/disable_iap_calls (2026-04-22)
|
||||||
|
|
||||||
|
## Corpus Check
|
||||||
|
- 8 files · ~284 words
|
||||||
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 11 nodes · 8 edges · 8 communities detected
|
||||||
|
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
|
||||||
|
- 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]]
|
||||||
|
|
||||||
|
## God Nodes (most connected - your core abstractions)
|
||||||
|
1. `IapAccountDisabled` - 2 edges
|
||||||
|
2. `get_credits()` - 2 edges
|
||||||
|
3. `DISABLED: Return fake unlimited credits` - 0 edges
|
||||||
|
|
||||||
|
## Surprising Connections (you probably didn't know these)
|
||||||
|
- None detected - all connections are within the same source files.
|
||||||
|
|
||||||
|
## Communities
|
||||||
|
|
||||||
|
### Community 0 - "Community 0"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): get_credits(), IapAccountDisabled
|
||||||
|
|
||||||
|
### Community 1 - "Community 1"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 2 - "Community 2"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 3 - "Community 3"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 4 - "Community 4"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 5 - "Community 5"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 6 - "Community 6"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return fake unlimited credits
|
||||||
|
|
||||||
|
### Community 7 - "Community 7"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
## Knowledge Gaps
|
||||||
|
- **1 isolated node(s):** `DISABLED: Return fake unlimited credits`
|
||||||
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
|
- **Thin community `Community 1`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 2`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 3`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 4`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 5`** (1 nodes): `__manifest__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 6`** (1 nodes): `DISABLED: Return fake unlimited credits`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 7`** (1 nodes): `__manifest__.py`
|
||||||
|
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:_
|
||||||
|
|
||||||
|
- **What connects `DISABLED: Return fake unlimited credits` to the rest of the system?**
|
||||||
|
_1 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py", "target": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py", "target": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py", "label": "iap_account.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L1"}, {"id": "iap_account_iapaccountdisabled", "label": "IapAccountDisabled", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L10"}, {"id": "iap_account_get_credits", "label": "get_credits()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L14"}, {"id": "iap_account_rationale_15", "label": "DISABLED: Return fake unlimited credits", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L15"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py", "target": "iap_account_iapaccountdisabled", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L10", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py", "target": "iap_account_get_credits", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L14", "weight": 1.0}, {"source": "iap_account_rationale_15", "target": "iap_account_iapaccountdisabled_get_credits", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L15", "weight": 1.0}], "raw_calls": [{"caller_nid": "iap_account_get_credits", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py", "source_location": "L18"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py", "label": "iap_account.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L1"}, {"id": "iap_account_iapaccountdisabled", "label": "IapAccountDisabled", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L10"}, {"id": "iap_account_get_credits", "label": "get_credits()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L14"}, {"id": "iap_account_rationale_15", "label": "DISABLED: Return fake unlimited credits", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L15"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py", "target": "iap_account_iapaccountdisabled", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L10", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py", "target": "iap_account_get_credits", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L14", "weight": 1.0}, {"source": "iap_account_rationale_15", "target": "iap_account_iapaccountdisabled_get_credits", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L15", "weight": 1.0}], "raw_calls": [{"caller_nid": "iap_account_get_credits", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py", "source_location": "L18"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
257
disable_iap_calls/graphify-out/graph.html
Normal file
257
disable_iap_calls/graphify-out/graph.html
Normal file
File diff suppressed because one or more lines are too long
205
disable_iap_calls/graphify-out/graph.json
Normal file
205
disable_iap_calls/graphify-out/graph.json
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
{
|
||||||
|
"directed": false,
|
||||||
|
"multigraph": false,
|
||||||
|
"graph": {},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py",
|
||||||
|
"community": 1,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__manifest__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/__manifest__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_manifest_py",
|
||||||
|
"community": 5,
|
||||||
|
"norm_label": "__manifest__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py",
|
||||||
|
"community": 2,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "iap_account.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "iap_account.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "IapAccountDisabled",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L10",
|
||||||
|
"id": "iap_account_iapaccountdisabled",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "iapaccountdisabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "get_credits()",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L14",
|
||||||
|
"id": "iap_account_get_credits",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "get_credits()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "DISABLED: Return fake unlimited credits",
|
||||||
|
"file_type": "rationale",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L15",
|
||||||
|
"id": "iap_account_rationale_15",
|
||||||
|
"community": 6,
|
||||||
|
"norm_label": "disabled: return fake unlimited credits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py",
|
||||||
|
"community": 3,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__manifest__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/__manifest__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_manifest_py",
|
||||||
|
"community": 7,
|
||||||
|
"norm_label": "__manifest__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py",
|
||||||
|
"community": 4,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "iap_account.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "iap_account.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_iap_calls_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L10",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py",
|
||||||
|
"_tgt": "iap_account_iapaccountdisabled",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py",
|
||||||
|
"target": "iap_account_iapaccountdisabled",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L14",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py",
|
||||||
|
"_tgt": "iap_account_get_credits",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_models_iap_account_py",
|
||||||
|
"target": "iap_account_get_credits",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L10",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py",
|
||||||
|
"_tgt": "iap_account_iapaccountdisabled",
|
||||||
|
"source": "iap_account_iapaccountdisabled",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/iap_account.py",
|
||||||
|
"source_location": "L14",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py",
|
||||||
|
"_tgt": "iap_account_get_credits",
|
||||||
|
"source": "iap_account_get_credits",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_iap_account_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_iap_calls/disable_iap_calls/models/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_iap_calls_disable_iap_calls_models_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hyperedges": []
|
||||||
|
}
|
||||||
3
disable_iap_calls/models/__init__.py
Normal file
3
disable_iap_calls/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import iap_account
|
||||||
|
|
||||||
20
disable_iap_calls/models/iap_account.py
Normal file
20
disable_iap_calls/models/iap_account.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Disable all IAP external API calls for local development
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IapAccountDisabled(models.Model):
|
||||||
|
_inherit = 'iap.account'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_credits(self, service_name):
|
||||||
|
"""
|
||||||
|
DISABLED: Return fake unlimited credits
|
||||||
|
"""
|
||||||
|
_logger.info("IAP get_credits DISABLED - returning unlimited credits for %s", service_name)
|
||||||
|
return 999999
|
||||||
|
|
||||||
143
disable_odoo_online/README.md
Normal file
143
disable_odoo_online/README.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Disable Odoo Online Services
|
||||||
|
|
||||||
|
**Version:** 18.0.1.0.0
|
||||||
|
**License:** LGPL-3
|
||||||
|
**Odoo Version:** 18.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This module comprehensively disables all external communications between your Odoo instance and Odoo's servers. It prevents:
|
||||||
|
|
||||||
|
- License/subscription checks
|
||||||
|
- User count reporting
|
||||||
|
- IAP (In-App Purchase) credit checks
|
||||||
|
- Publisher warranty communications
|
||||||
|
- Partner autocomplete/enrichment
|
||||||
|
- Expiration warnings in the UI
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. IAP JSON-RPC Blocking
|
||||||
|
Patches the core `iap_jsonrpc` function to prevent all IAP API calls:
|
||||||
|
- Returns fake successful responses
|
||||||
|
- Logs all blocked calls
|
||||||
|
- Provides unlimited credits for services that check
|
||||||
|
|
||||||
|
### 2. License Parameter Protection
|
||||||
|
Protects critical `ir.config_parameter` values:
|
||||||
|
- `database.expiration_date` → Always returns `2099-12-31 23:59:59`
|
||||||
|
- `database.expiration_reason` → Always returns `renewal`
|
||||||
|
- `database.enterprise_code` → Always returns `PERMANENT_LOCAL`
|
||||||
|
|
||||||
|
### 3. Session Info Patching
|
||||||
|
Modifies `session_info()` to prevent frontend warnings:
|
||||||
|
- Sets expiration date to 2099
|
||||||
|
- Sets `warning` to `False`
|
||||||
|
- Removes "already linked" subscription prompts
|
||||||
|
|
||||||
|
### 4. User Creation Protection
|
||||||
|
Logs user creation without triggering subscription checks:
|
||||||
|
- Blocks any external validation
|
||||||
|
- Logs permission changes
|
||||||
|
|
||||||
|
### 5. Publisher Warranty Block
|
||||||
|
Disables all warranty-related server communication:
|
||||||
|
- `_get_sys_logs()` → Returns empty response
|
||||||
|
- `update_notification()` → Returns success without calling server
|
||||||
|
|
||||||
|
### 6. Cron Job Blocking
|
||||||
|
Blocks scheduled actions that contact Odoo:
|
||||||
|
- Publisher Warranty Check
|
||||||
|
- Database Auto-Expiration Check
|
||||||
|
- Various IAP-related crons
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy the module to your Odoo addons directory
|
||||||
|
2. Restart Odoo
|
||||||
|
3. Go to Apps → Update Apps List
|
||||||
|
4. Search for "Disable Odoo Online Services"
|
||||||
|
5. Click Install
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Check that blocking is active:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs odoo-container 2>&1 | grep -i "BLOCKED\|DISABLED"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
IAP JSON-RPC calls have been DISABLED globally
|
||||||
|
Module update_list: Scanning local addons only (Odoo Apps store disabled)
|
||||||
|
Publisher warranty update_notification BLOCKED
|
||||||
|
Creating 1 user(s) - subscription check DISABLED
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration required. The module automatically:
|
||||||
|
- Sets permanent expiration values on install (via `_post_init_hook`)
|
||||||
|
- Patches all necessary functions when loaded
|
||||||
|
- Protects values from being changed
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `models/disable_iap_tools.py` | Patches `iap_jsonrpc` globally |
|
||||||
|
| `models/disable_online_services.py` | Blocks publisher warranty, cron jobs |
|
||||||
|
| `models/disable_database_expiration.py` | Protects `ir.config_parameter` |
|
||||||
|
| `models/disable_session_leaks.py` | Patches session info, user creation |
|
||||||
|
| `models/disable_partner_autocomplete.py` | Blocks partner enrichment |
|
||||||
|
| `models/disable_all_external.py` | Additional external call blocks |
|
||||||
|
|
||||||
|
### Blocked Endpoints
|
||||||
|
|
||||||
|
All redirected to `http://localhost:65535`:
|
||||||
|
|
||||||
|
- `iap.endpoint`
|
||||||
|
- `publisher_warranty_url`
|
||||||
|
- `partner_autocomplete.endpoint`
|
||||||
|
- `iap_extract_endpoint`
|
||||||
|
- `olg.endpoint`
|
||||||
|
- `mail.media_library_endpoint`
|
||||||
|
- `sms.endpoint`
|
||||||
|
- `crm.iap_lead_mining.endpoint`
|
||||||
|
- And many more...
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `base`
|
||||||
|
- `web`
|
||||||
|
- `iap`
|
||||||
|
- `mail`
|
||||||
|
- `base_setup`
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- Odoo 18.0 Community Edition
|
||||||
|
- Odoo 18.0 Enterprise Edition
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This module is intended for legitimate use cases such as:
|
||||||
|
- Air-gapped environments
|
||||||
|
- Development/testing instances
|
||||||
|
- Self-hosted deployments with proper licensing
|
||||||
|
|
||||||
|
Ensure you comply with Odoo's licensing terms for your use case.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 1.0.0 (2025-12-29)
|
||||||
|
- Initial release
|
||||||
|
- IAP blocking
|
||||||
|
- Publisher warranty blocking
|
||||||
|
- Session info patching
|
||||||
|
- User creation protection
|
||||||
|
- Config parameter protection
|
||||||
|
|
||||||
67
disable_odoo_online/__init__.py
Normal file
67
disable_odoo_online/__init__.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
def _post_init_hook(env):
|
||||||
|
"""
|
||||||
|
Set all configuration parameters to disable external Odoo services.
|
||||||
|
This runs after module installation.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
set_param = env['ir.config_parameter'].sudo().set_param
|
||||||
|
|
||||||
|
# Set permanent database expiration
|
||||||
|
params_to_set = {
|
||||||
|
# Database license parameters
|
||||||
|
'database.expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'database.expiration_reason': 'renewal',
|
||||||
|
'database.enterprise_code': 'PERMANENT_LOCAL',
|
||||||
|
|
||||||
|
# Clear "already linked" parameters
|
||||||
|
'database.already_linked_subscription_url': '',
|
||||||
|
'database.already_linked_email': '',
|
||||||
|
'database.already_linked_send_mail_url': '',
|
||||||
|
|
||||||
|
# Redirect all IAP endpoints to localhost
|
||||||
|
'iap.endpoint': 'http://localhost:65535',
|
||||||
|
'partner_autocomplete.endpoint': 'http://localhost:65535',
|
||||||
|
'iap_extract_endpoint': 'http://localhost:65535',
|
||||||
|
'olg.endpoint': 'http://localhost:65535',
|
||||||
|
'mail.media_library_endpoint': 'http://localhost:65535',
|
||||||
|
'website.api_endpoint': 'http://localhost:65535',
|
||||||
|
'sms.endpoint': 'http://localhost:65535',
|
||||||
|
'crm.iap_lead_mining.endpoint': 'http://localhost:65535',
|
||||||
|
'reveal.endpoint': 'http://localhost:65535',
|
||||||
|
'publisher_warranty_url': 'http://localhost:65535',
|
||||||
|
|
||||||
|
# OCN (Odoo Cloud Notification) - blocks push notifications to Odoo
|
||||||
|
'odoo_ocn.endpoint': 'http://localhost:65535', # Main OCN endpoint
|
||||||
|
'mail_mobile.enable_ocn': 'False', # Disable OCN push notifications
|
||||||
|
'odoo_ocn.project_id': '', # Clear any registered project
|
||||||
|
'ocn.uuid': '', # Clear OCN UUID to prevent registration
|
||||||
|
|
||||||
|
# Snailmail (physical mail service)
|
||||||
|
'snailmail.endpoint': 'http://localhost:65535',
|
||||||
|
|
||||||
|
# Social media IAP
|
||||||
|
'social.facebook_endpoint': 'http://localhost:65535',
|
||||||
|
'social.twitter_endpoint': 'http://localhost:65535',
|
||||||
|
'social.linkedin_endpoint': 'http://localhost:65535',
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.info("=" * 60)
|
||||||
|
_logger.info("DISABLE ODOO ONLINE: Setting configuration parameters")
|
||||||
|
_logger.info("=" * 60)
|
||||||
|
|
||||||
|
for key, value in params_to_set.items():
|
||||||
|
try:
|
||||||
|
set_param(key, value)
|
||||||
|
_logger.info("Set %s = %s", key, value if len(str(value)) < 30 else value[:30] + "...")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not set %s: %s", key, e)
|
||||||
|
|
||||||
|
_logger.info("=" * 60)
|
||||||
|
_logger.info("DISABLE ODOO ONLINE: Configuration complete")
|
||||||
|
_logger.info("=" * 60)
|
||||||
56
disable_odoo_online/__manifest__.py
Normal file
56
disable_odoo_online/__manifest__.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Disable Odoo Online Services',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Tools',
|
||||||
|
'summary': 'Blocks ALL external Odoo server communications',
|
||||||
|
'description': """
|
||||||
|
Comprehensive Module to Disable ALL Odoo Online Services
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
This module completely blocks all external communications from Odoo to Odoo's servers.
|
||||||
|
|
||||||
|
**Blocked Services:**
|
||||||
|
- Publisher Warranty checks (license validation)
|
||||||
|
- IAP (In-App Purchase) - All services
|
||||||
|
- Partner Autocomplete API
|
||||||
|
- Company Enrichment API
|
||||||
|
- VAT Lookup API
|
||||||
|
- SMS API
|
||||||
|
- Invoice/Expense OCR Extract
|
||||||
|
- Media Library (Stock Images)
|
||||||
|
- Currency Rate Live Updates
|
||||||
|
- CRM Lead Mining
|
||||||
|
- CRM Reveal (Website visitor identification)
|
||||||
|
- Google Calendar Sync
|
||||||
|
- AI/OLG Content Generation
|
||||||
|
- Database Registration
|
||||||
|
- Module Update checks from Odoo Store
|
||||||
|
- Session-based license detection
|
||||||
|
- Frontend expiration panel warnings
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Air-gapped installations
|
||||||
|
- Local development without internet
|
||||||
|
- Enterprise deployments that don't want telemetry
|
||||||
|
- Testing environments
|
||||||
|
|
||||||
|
**WARNING:** This module disables legitimate Odoo services.
|
||||||
|
Only use if you understand the implications.
|
||||||
|
""",
|
||||||
|
'author': 'Fusion Development',
|
||||||
|
'website': 'https://fusiondevelopment.com',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'depends': ['base', 'mail', 'web'],
|
||||||
|
'data': [
|
||||||
|
'data/disable_external_services.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'disable_odoo_online/static/src/js/disable_external_links.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'application': False,
|
||||||
|
}
|
||||||
5
disable_odoo_online/data/disable_external_services.xml
Normal file
5
disable_odoo_online/data/disable_external_services.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<!-- All config parameters are set via post_init_hook in __init__.py -->
|
||||||
|
<!-- This file is kept for future data records if needed -->
|
||||||
|
</odoo>
|
||||||
143
disable_odoo_online/disable_odoo_online/README.md
Normal file
143
disable_odoo_online/disable_odoo_online/README.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Disable Odoo Online Services
|
||||||
|
|
||||||
|
**Version:** 18.0.1.0.0
|
||||||
|
**License:** LGPL-3
|
||||||
|
**Odoo Version:** 18.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This module comprehensively disables all external communications between your Odoo instance and Odoo's servers. It prevents:
|
||||||
|
|
||||||
|
- License/subscription checks
|
||||||
|
- User count reporting
|
||||||
|
- IAP (In-App Purchase) credit checks
|
||||||
|
- Publisher warranty communications
|
||||||
|
- Partner autocomplete/enrichment
|
||||||
|
- Expiration warnings in the UI
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. IAP JSON-RPC Blocking
|
||||||
|
Patches the core `iap_jsonrpc` function to prevent all IAP API calls:
|
||||||
|
- Returns fake successful responses
|
||||||
|
- Logs all blocked calls
|
||||||
|
- Provides unlimited credits for services that check
|
||||||
|
|
||||||
|
### 2. License Parameter Protection
|
||||||
|
Protects critical `ir.config_parameter` values:
|
||||||
|
- `database.expiration_date` → Always returns `2099-12-31 23:59:59`
|
||||||
|
- `database.expiration_reason` → Always returns `renewal`
|
||||||
|
- `database.enterprise_code` → Always returns `PERMANENT_LOCAL`
|
||||||
|
|
||||||
|
### 3. Session Info Patching
|
||||||
|
Modifies `session_info()` to prevent frontend warnings:
|
||||||
|
- Sets expiration date to 2099
|
||||||
|
- Sets `warning` to `False`
|
||||||
|
- Removes "already linked" subscription prompts
|
||||||
|
|
||||||
|
### 4. User Creation Protection
|
||||||
|
Logs user creation without triggering subscription checks:
|
||||||
|
- Blocks any external validation
|
||||||
|
- Logs permission changes
|
||||||
|
|
||||||
|
### 5. Publisher Warranty Block
|
||||||
|
Disables all warranty-related server communication:
|
||||||
|
- `_get_sys_logs()` → Returns empty response
|
||||||
|
- `update_notification()` → Returns success without calling server
|
||||||
|
|
||||||
|
### 6. Cron Job Blocking
|
||||||
|
Blocks scheduled actions that contact Odoo:
|
||||||
|
- Publisher Warranty Check
|
||||||
|
- Database Auto-Expiration Check
|
||||||
|
- Various IAP-related crons
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy the module to your Odoo addons directory
|
||||||
|
2. Restart Odoo
|
||||||
|
3. Go to Apps → Update Apps List
|
||||||
|
4. Search for "Disable Odoo Online Services"
|
||||||
|
5. Click Install
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Check that blocking is active:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs odoo-container 2>&1 | grep -i "BLOCKED\|DISABLED"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
IAP JSON-RPC calls have been DISABLED globally
|
||||||
|
Module update_list: Scanning local addons only (Odoo Apps store disabled)
|
||||||
|
Publisher warranty update_notification BLOCKED
|
||||||
|
Creating 1 user(s) - subscription check DISABLED
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration required. The module automatically:
|
||||||
|
- Sets permanent expiration values on install (via `_post_init_hook`)
|
||||||
|
- Patches all necessary functions when loaded
|
||||||
|
- Protects values from being changed
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `models/disable_iap_tools.py` | Patches `iap_jsonrpc` globally |
|
||||||
|
| `models/disable_online_services.py` | Blocks publisher warranty, cron jobs |
|
||||||
|
| `models/disable_database_expiration.py` | Protects `ir.config_parameter` |
|
||||||
|
| `models/disable_session_leaks.py` | Patches session info, user creation |
|
||||||
|
| `models/disable_partner_autocomplete.py` | Blocks partner enrichment |
|
||||||
|
| `models/disable_all_external.py` | Additional external call blocks |
|
||||||
|
|
||||||
|
### Blocked Endpoints
|
||||||
|
|
||||||
|
All redirected to `http://localhost:65535`:
|
||||||
|
|
||||||
|
- `iap.endpoint`
|
||||||
|
- `publisher_warranty_url`
|
||||||
|
- `partner_autocomplete.endpoint`
|
||||||
|
- `iap_extract_endpoint`
|
||||||
|
- `olg.endpoint`
|
||||||
|
- `mail.media_library_endpoint`
|
||||||
|
- `sms.endpoint`
|
||||||
|
- `crm.iap_lead_mining.endpoint`
|
||||||
|
- And many more...
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `base`
|
||||||
|
- `web`
|
||||||
|
- `iap`
|
||||||
|
- `mail`
|
||||||
|
- `base_setup`
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- Odoo 18.0 Community Edition
|
||||||
|
- Odoo 18.0 Enterprise Edition
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This module is intended for legitimate use cases such as:
|
||||||
|
- Air-gapped environments
|
||||||
|
- Development/testing instances
|
||||||
|
- Self-hosted deployments with proper licensing
|
||||||
|
|
||||||
|
Ensure you comply with Odoo's licensing terms for your use case.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 1.0.0 (2025-12-29)
|
||||||
|
- Initial release
|
||||||
|
- IAP blocking
|
||||||
|
- Publisher warranty blocking
|
||||||
|
- Session info patching
|
||||||
|
- User creation protection
|
||||||
|
- Config parameter protection
|
||||||
|
|
||||||
67
disable_odoo_online/disable_odoo_online/__init__.py
Normal file
67
disable_odoo_online/disable_odoo_online/__init__.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
def _post_init_hook(env):
|
||||||
|
"""
|
||||||
|
Set all configuration parameters to disable external Odoo services.
|
||||||
|
This runs after module installation.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
set_param = env['ir.config_parameter'].sudo().set_param
|
||||||
|
|
||||||
|
# Set permanent database expiration
|
||||||
|
params_to_set = {
|
||||||
|
# Database license parameters
|
||||||
|
'database.expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'database.expiration_reason': 'renewal',
|
||||||
|
'database.enterprise_code': 'PERMANENT_LOCAL',
|
||||||
|
|
||||||
|
# Clear "already linked" parameters
|
||||||
|
'database.already_linked_subscription_url': '',
|
||||||
|
'database.already_linked_email': '',
|
||||||
|
'database.already_linked_send_mail_url': '',
|
||||||
|
|
||||||
|
# Redirect all IAP endpoints to localhost
|
||||||
|
'iap.endpoint': 'http://localhost:65535',
|
||||||
|
'partner_autocomplete.endpoint': 'http://localhost:65535',
|
||||||
|
'iap_extract_endpoint': 'http://localhost:65535',
|
||||||
|
'olg.endpoint': 'http://localhost:65535',
|
||||||
|
'mail.media_library_endpoint': 'http://localhost:65535',
|
||||||
|
'website.api_endpoint': 'http://localhost:65535',
|
||||||
|
'sms.endpoint': 'http://localhost:65535',
|
||||||
|
'crm.iap_lead_mining.endpoint': 'http://localhost:65535',
|
||||||
|
'reveal.endpoint': 'http://localhost:65535',
|
||||||
|
'publisher_warranty_url': 'http://localhost:65535',
|
||||||
|
|
||||||
|
# OCN (Odoo Cloud Notification) - blocks push notifications to Odoo
|
||||||
|
'odoo_ocn.endpoint': 'http://localhost:65535', # Main OCN endpoint
|
||||||
|
'mail_mobile.enable_ocn': 'False', # Disable OCN push notifications
|
||||||
|
'odoo_ocn.project_id': '', # Clear any registered project
|
||||||
|
'ocn.uuid': '', # Clear OCN UUID to prevent registration
|
||||||
|
|
||||||
|
# Snailmail (physical mail service)
|
||||||
|
'snailmail.endpoint': 'http://localhost:65535',
|
||||||
|
|
||||||
|
# Social media IAP
|
||||||
|
'social.facebook_endpoint': 'http://localhost:65535',
|
||||||
|
'social.twitter_endpoint': 'http://localhost:65535',
|
||||||
|
'social.linkedin_endpoint': 'http://localhost:65535',
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.info("=" * 60)
|
||||||
|
_logger.info("DISABLE ODOO ONLINE: Setting configuration parameters")
|
||||||
|
_logger.info("=" * 60)
|
||||||
|
|
||||||
|
for key, value in params_to_set.items():
|
||||||
|
try:
|
||||||
|
set_param(key, value)
|
||||||
|
_logger.info("Set %s = %s", key, value if len(str(value)) < 30 else value[:30] + "...")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not set %s: %s", key, e)
|
||||||
|
|
||||||
|
_logger.info("=" * 60)
|
||||||
|
_logger.info("DISABLE ODOO ONLINE: Configuration complete")
|
||||||
|
_logger.info("=" * 60)
|
||||||
56
disable_odoo_online/disable_odoo_online/__manifest__.py
Normal file
56
disable_odoo_online/disable_odoo_online/__manifest__.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Disable Odoo Online Services',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Tools',
|
||||||
|
'summary': 'Blocks ALL external Odoo server communications',
|
||||||
|
'description': """
|
||||||
|
Comprehensive Module to Disable ALL Odoo Online Services
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
This module completely blocks all external communications from Odoo to Odoo's servers.
|
||||||
|
|
||||||
|
**Blocked Services:**
|
||||||
|
- Publisher Warranty checks (license validation)
|
||||||
|
- IAP (In-App Purchase) - All services
|
||||||
|
- Partner Autocomplete API
|
||||||
|
- Company Enrichment API
|
||||||
|
- VAT Lookup API
|
||||||
|
- SMS API
|
||||||
|
- Invoice/Expense OCR Extract
|
||||||
|
- Media Library (Stock Images)
|
||||||
|
- Currency Rate Live Updates
|
||||||
|
- CRM Lead Mining
|
||||||
|
- CRM Reveal (Website visitor identification)
|
||||||
|
- Google Calendar Sync
|
||||||
|
- AI/OLG Content Generation
|
||||||
|
- Database Registration
|
||||||
|
- Module Update checks from Odoo Store
|
||||||
|
- Session-based license detection
|
||||||
|
- Frontend expiration panel warnings
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Air-gapped installations
|
||||||
|
- Local development without internet
|
||||||
|
- Enterprise deployments that don't want telemetry
|
||||||
|
- Testing environments
|
||||||
|
|
||||||
|
**WARNING:** This module disables legitimate Odoo services.
|
||||||
|
Only use if you understand the implications.
|
||||||
|
""",
|
||||||
|
'author': 'Fusion Development',
|
||||||
|
'website': 'https://fusiondevelopment.com',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'depends': ['base', 'mail', 'web'],
|
||||||
|
'data': [
|
||||||
|
'data/disable_external_services.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'disable_odoo_online/static/src/js/disable_external_links.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'application': False,
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<!-- All config parameters are set via post_init_hook in __init__.py -->
|
||||||
|
<!-- This file is kept for future data records if needed -->
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import disable_iap_tools # Patches iap_jsonrpc globally - MUST be first
|
||||||
|
from . import disable_http_requests # Patches requests library to block Odoo domains
|
||||||
|
from . import disable_online_services
|
||||||
|
from . import disable_partner_autocomplete
|
||||||
|
from . import disable_database_expiration
|
||||||
|
from . import disable_all_external
|
||||||
|
from . import disable_session_leaks
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Comprehensive blocking of ALL external Odoo service calls.
|
||||||
|
Only inherits from models that are guaranteed to exist in base Odoo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Block Currency Rate Live Updates - Uses res.currency which always exists
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class ResCurrencyDisabled(models.Model):
|
||||||
|
_inherit = 'res.currency'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_rates_from_provider(self, provider, date):
|
||||||
|
"""DISABLED: Return empty rates."""
|
||||||
|
_logger.debug("Currency rate provider BLOCKED: provider=%s", provider)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Block Gravatar - Uses res.partner which always exists
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class ResPartnerDisabled(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_gravatar_image(self, email):
|
||||||
|
"""DISABLED: Return False to skip gravatar lookup."""
|
||||||
|
_logger.debug("Gravatar lookup BLOCKED for email=%s", email)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Disable database expiration checks and registration.
|
||||||
|
Consolidates all ir.config_parameter overrides.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrConfigParameter(models.Model):
|
||||||
|
"""Override config parameters to prevent expiration and protect license values."""
|
||||||
|
_inherit = 'ir.config_parameter'
|
||||||
|
|
||||||
|
PROTECTED_PARAMS = {
|
||||||
|
'database.expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'database.expiration_reason': 'renewal',
|
||||||
|
'database.enterprise_code': 'PERMANENT_LOCAL',
|
||||||
|
}
|
||||||
|
|
||||||
|
CLEAR_PARAMS = [
|
||||||
|
'database.already_linked_subscription_url',
|
||||||
|
'database.already_linked_email',
|
||||||
|
'database.already_linked_send_mail_url',
|
||||||
|
]
|
||||||
|
|
||||||
|
def init(self, force=False):
|
||||||
|
"""Set permanent valid subscription on module init."""
|
||||||
|
super().init(force=force)
|
||||||
|
self._set_permanent_subscription()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _set_permanent_subscription(self):
|
||||||
|
"""Set database to never expire."""
|
||||||
|
_logger.info("Setting permanent subscription values...")
|
||||||
|
|
||||||
|
for key, value in self.PROTECTED_PARAMS.items():
|
||||||
|
try:
|
||||||
|
self.env.cr.execute("""
|
||||||
|
INSERT INTO ir_config_parameter (key, value, create_uid, create_date, write_uid, write_date)
|
||||||
|
VALUES (%s, %s, %s, NOW() AT TIME ZONE 'UTC', %s, NOW() AT TIME ZONE 'UTC')
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = %s, write_date = NOW() AT TIME ZONE 'UTC'
|
||||||
|
""", (key, value, self.env.uid, self.env.uid, value))
|
||||||
|
except Exception as e:
|
||||||
|
_logger.debug("Could not set param %s: %s", key, e)
|
||||||
|
|
||||||
|
for key in self.CLEAR_PARAMS:
|
||||||
|
try:
|
||||||
|
self.env.cr.execute("""
|
||||||
|
INSERT INTO ir_config_parameter (key, value, create_uid, create_date, write_uid, write_date)
|
||||||
|
VALUES (%s, '', %s, NOW() AT TIME ZONE 'UTC', %s, NOW() AT TIME ZONE 'UTC')
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = '', write_date = NOW() AT TIME ZONE 'UTC'
|
||||||
|
""", (key, self.env.uid, self.env.uid))
|
||||||
|
except Exception as e:
|
||||||
|
_logger.debug("Could not clear param %s: %s", key, e)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_param(self, key, default=False):
|
||||||
|
"""Override get_param to return permanent values for protected params."""
|
||||||
|
if key in self.PROTECTED_PARAMS:
|
||||||
|
return self.PROTECTED_PARAMS[key]
|
||||||
|
|
||||||
|
if key in self.CLEAR_PARAMS:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return super().get_param(key, default)
|
||||||
|
|
||||||
|
def set_param(self, key, value):
|
||||||
|
"""Override set_param to prevent external processes from changing protected values."""
|
||||||
|
if key in self.PROTECTED_PARAMS:
|
||||||
|
if value != self.PROTECTED_PARAMS[key]:
|
||||||
|
_logger.warning("Blocked attempt to change protected param %s to %s", key, value)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if key in self.CLEAR_PARAMS:
|
||||||
|
value = ''
|
||||||
|
|
||||||
|
return super().set_param(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseExpirationCheck(models.AbstractModel):
|
||||||
|
_name = 'disable.odoo.online.expiration'
|
||||||
|
_description = 'Database Expiration Blocker'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def check_database_expiration(self):
|
||||||
|
return {
|
||||||
|
'valid': True,
|
||||||
|
'expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'expiration_reason': 'renewal',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Base(models.AbstractModel):
|
||||||
|
_inherit = 'base'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_database_expiration_date(self):
|
||||||
|
return datetime(2099, 12, 31, 23, 59, 59)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _check_database_enterprise_expiration(self):
|
||||||
|
return True
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Block ALL outgoing HTTP requests to Odoo-related domains.
|
||||||
|
This patches the requests library to intercept and block external calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from functools import wraps
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Domains to block - all Odoo external services
|
||||||
|
BLOCKED_DOMAINS = [
|
||||||
|
'odoo.com',
|
||||||
|
'odoofin.com',
|
||||||
|
'odoo.sh',
|
||||||
|
'iap.odoo.com',
|
||||||
|
'iap-services.odoo.com',
|
||||||
|
'partner-autocomplete.odoo.com',
|
||||||
|
'iap-extract.odoo.com',
|
||||||
|
'iap-sms.odoo.com',
|
||||||
|
'upgrade.odoo.com',
|
||||||
|
'apps.odoo.com',
|
||||||
|
'production.odoofin.com',
|
||||||
|
'plaid.com',
|
||||||
|
'yodlee.com',
|
||||||
|
'gravatar.com',
|
||||||
|
'www.gravatar.com',
|
||||||
|
'secure.gravatar.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Store original functions
|
||||||
|
_original_request = None
|
||||||
|
_original_get = None
|
||||||
|
_original_post = None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_blocked_url(url):
|
||||||
|
"""Check if the URL should be blocked."""
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
domain = parsed.netloc.lower()
|
||||||
|
for blocked in BLOCKED_DOMAINS:
|
||||||
|
if blocked in domain:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_request(method, url, **kwargs):
|
||||||
|
"""Intercept and block requests to Odoo domains."""
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP REQUEST BLOCKED: %s %s", method.upper(), url)
|
||||||
|
# Return a mock response
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
return response
|
||||||
|
return _original_request(method, url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_get(url, **kwargs):
|
||||||
|
"""Intercept and block GET requests."""
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP GET BLOCKED: %s", url)
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
return response
|
||||||
|
return _original_get(url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_post(url, **kwargs):
|
||||||
|
"""Intercept and block POST requests."""
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP POST BLOCKED: %s", url)
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
return response
|
||||||
|
return _original_post(url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_requests():
|
||||||
|
"""Monkey-patch requests library to block Odoo domains."""
|
||||||
|
global _original_request, _original_get, _original_post
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _original_request is None:
|
||||||
|
_original_request = requests.Session.request
|
||||||
|
_original_get = requests.get
|
||||||
|
_original_post = requests.post
|
||||||
|
|
||||||
|
# Patch Session.request (catches most calls)
|
||||||
|
def patched_session_request(self, method, url, **kwargs):
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP SESSION REQUEST BLOCKED: %s %s", method.upper(), url)
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
response.request = requests.models.PreparedRequest()
|
||||||
|
response.request.url = url
|
||||||
|
response.request.method = method
|
||||||
|
return response
|
||||||
|
return _original_request(self, method, url, **kwargs)
|
||||||
|
|
||||||
|
requests.Session.request = patched_session_request
|
||||||
|
requests.get = _blocked_get
|
||||||
|
requests.post = _blocked_post
|
||||||
|
|
||||||
|
_logger.info("HTTP requests to Odoo domains have been BLOCKED")
|
||||||
|
_logger.info("Blocked domains: %s", ', '.join(BLOCKED_DOMAINS))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not patch requests library: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# Apply patch when module is imported
|
||||||
|
patch_requests()
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Override the core IAP tools to block ALL external API calls.
|
||||||
|
This is the master switch that blocks ALL Odoo external communications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import exceptions, _
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Store original function reference
|
||||||
|
_original_iap_jsonrpc = None
|
||||||
|
|
||||||
|
|
||||||
|
def _disabled_iap_jsonrpc(url, method='call', params=None, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Block all IAP JSON-RPC calls.
|
||||||
|
Returns empty/success response instead of making external calls.
|
||||||
|
"""
|
||||||
|
_logger.info("IAP JSONRPC BLOCKED: %s (method=%s)", url, method)
|
||||||
|
|
||||||
|
# Return appropriate empty responses based on the endpoint
|
||||||
|
if '/authorize' in url:
|
||||||
|
return 'fake_transaction_token_disabled'
|
||||||
|
elif '/capture' in url or '/cancel' in url:
|
||||||
|
return True
|
||||||
|
elif '/credits' in url:
|
||||||
|
return 999999
|
||||||
|
elif 'partner-autocomplete' in url:
|
||||||
|
return []
|
||||||
|
elif 'enrich' in url:
|
||||||
|
return {}
|
||||||
|
elif 'sms' in url:
|
||||||
|
_logger.warning("SMS API call blocked - SMS will not be sent")
|
||||||
|
return {'state': 'success', 'credits': 999999}
|
||||||
|
elif 'extract' in url:
|
||||||
|
return {'status': 'success', 'credits': 999999}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def patch_iap_tools():
|
||||||
|
"""
|
||||||
|
Monkey-patch the iap_jsonrpc function to block external calls.
|
||||||
|
This is called when the module loads.
|
||||||
|
"""
|
||||||
|
global _original_iap_jsonrpc
|
||||||
|
|
||||||
|
try:
|
||||||
|
from odoo.addons.iap.tools import iap_tools
|
||||||
|
|
||||||
|
if _original_iap_jsonrpc is None:
|
||||||
|
_original_iap_jsonrpc = iap_tools.iap_jsonrpc
|
||||||
|
|
||||||
|
iap_tools.iap_jsonrpc = _disabled_iap_jsonrpc
|
||||||
|
_logger.info("IAP JSON-RPC calls have been DISABLED globally")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
_logger.debug("IAP module not installed, skipping patch")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not patch IAP tools: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# Apply patch when module is imported
|
||||||
|
patch_iap_tools()
|
||||||
|
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Disable various Odoo online services and external API calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrModuleModule(models.Model):
|
||||||
|
"""Disable module update checks from Odoo store."""
|
||||||
|
_inherit = 'ir.module.module'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def update_list(self):
|
||||||
|
"""
|
||||||
|
Override to prevent fetching from Odoo Apps store.
|
||||||
|
Only scan local addons paths.
|
||||||
|
"""
|
||||||
|
_logger.info("Module update_list: Scanning local addons only (Odoo Apps store disabled)")
|
||||||
|
return super().update_list()
|
||||||
|
|
||||||
|
def button_immediate_upgrade(self):
|
||||||
|
"""Prevent upgrade attempts that might contact Odoo."""
|
||||||
|
_logger.info("Module upgrade: Processing locally only")
|
||||||
|
return super().button_immediate_upgrade()
|
||||||
|
|
||||||
|
|
||||||
|
class IrCron(models.Model):
|
||||||
|
"""Disable scheduled actions that contact Odoo servers."""
|
||||||
|
_inherit = 'ir.cron'
|
||||||
|
|
||||||
|
def _callback(self, cron_name, server_action_id):
|
||||||
|
"""
|
||||||
|
Override to block certain cron jobs that contact Odoo.
|
||||||
|
Odoo 19 signature: _callback(self, cron_name, server_action_id)
|
||||||
|
"""
|
||||||
|
blocked_crons = [
|
||||||
|
'publisher',
|
||||||
|
'warranty',
|
||||||
|
'update_notification',
|
||||||
|
'database_expiration',
|
||||||
|
'iap_enrich',
|
||||||
|
'ocr',
|
||||||
|
'Invoice OCR',
|
||||||
|
'enrich leads',
|
||||||
|
'fetchmail',
|
||||||
|
'online sync',
|
||||||
|
]
|
||||||
|
|
||||||
|
cron_lower = (cron_name or '').lower()
|
||||||
|
for blocked in blocked_crons:
|
||||||
|
if blocked.lower() in cron_lower:
|
||||||
|
_logger.info("Cron BLOCKED (external call): %s", cron_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return super()._callback(cron_name, server_action_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
"""Override config settings to prevent external service configuration."""
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
def set_values(self):
|
||||||
|
"""Ensure certain settings stay disabled."""
|
||||||
|
res = super().set_values()
|
||||||
|
|
||||||
|
# Disable any auto-update settings and set permanent expiration
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
params.set_param('database.expiration_date', '2099-12-31 23:59:59')
|
||||||
|
params.set_param('database.expiration_reason', 'renewal')
|
||||||
|
params.set_param('database.enterprise_code', 'PERMANENT_LOCAL')
|
||||||
|
|
||||||
|
# Disable IAP endpoint (redirect to nowhere)
|
||||||
|
params.set_param('iap.endpoint', 'http://localhost:65535')
|
||||||
|
|
||||||
|
# Disable various external services
|
||||||
|
params.set_param('partner_autocomplete.endpoint', 'http://localhost:65535')
|
||||||
|
params.set_param('iap_extract_endpoint', 'http://localhost:65535')
|
||||||
|
params.set_param('olg.endpoint', 'http://localhost:65535')
|
||||||
|
params.set_param('mail.media_library_endpoint', 'http://localhost:65535')
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class PublisherWarrantyContract(models.AbstractModel):
|
||||||
|
"""Completely disable publisher warranty checks."""
|
||||||
|
_inherit = 'publisher_warranty.contract'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_sys_logs(self):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not contact Odoo servers.
|
||||||
|
Returns fake successful response.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty _get_sys_logs BLOCKED")
|
||||||
|
return {
|
||||||
|
'messages': [],
|
||||||
|
'enterprise_info': {
|
||||||
|
'expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'expiration_reason': 'renewal',
|
||||||
|
'enterprise_code': 'PERMANENT_LOCAL',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_message(self):
|
||||||
|
"""DISABLED: Return empty message."""
|
||||||
|
_logger.info("Publisher warranty _get_message BLOCKED")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_notification(self, cron_mode=True):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not send any data to Odoo servers.
|
||||||
|
Just update local parameters with permanent values.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty update_notification BLOCKED")
|
||||||
|
|
||||||
|
# Set permanent valid subscription parameters
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
params.set_param('database.expiration_date', '2099-12-31 23:59:59')
|
||||||
|
params.set_param('database.expiration_reason', 'renewal')
|
||||||
|
params.set_param('database.enterprise_code', 'PERMANENT_LOCAL')
|
||||||
|
|
||||||
|
# Clear any "already linked" parameters
|
||||||
|
params.set_param('database.already_linked_subscription_url', '')
|
||||||
|
params.set_param('database.already_linked_email', '')
|
||||||
|
params.set_param('database.already_linked_send_mail_url', '')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
"""Block certain routes that call external services."""
|
||||||
|
_inherit = 'ir.http'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pre_dispatch(cls, rule, arguments):
|
||||||
|
"""Log and potentially block external service routes."""
|
||||||
|
# List of route patterns that should be blocked
|
||||||
|
blocked_routes = [
|
||||||
|
'/iap/',
|
||||||
|
'/partner_autocomplete/',
|
||||||
|
'/google_',
|
||||||
|
'/ocr/',
|
||||||
|
'/sms/',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Note: We don't actually block here as it might break functionality
|
||||||
|
# The actual blocking happens at the API/model level
|
||||||
|
return super()._pre_dispatch(rule, arguments)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Disable Partner Autocomplete external API calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
"""Disable partner autocomplete from Odoo API."""
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def autocomplete(self, query, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty results instead of calling Odoo's partner API.
|
||||||
|
"""
|
||||||
|
_logger.debug("Partner autocomplete DISABLED - returning empty results for: %s", query)
|
||||||
|
return []
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def enrich_company(self, company_domain, partner_gid, vat, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty data instead of calling Odoo's enrichment API.
|
||||||
|
"""
|
||||||
|
_logger.debug("Partner enrichment DISABLED - returning empty for domain: %s", company_domain)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def read_by_vat(self, vat, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty data instead of calling Odoo's VAT lookup API.
|
||||||
|
"""
|
||||||
|
_logger.debug("Partner VAT lookup DISABLED - returning empty for VAT: %s", vat)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
"""Disable company autocomplete features."""
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def autocomplete(self, query, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty results for company autocomplete.
|
||||||
|
"""
|
||||||
|
_logger.debug("Company autocomplete DISABLED - returning empty results")
|
||||||
|
return []
|
||||||
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Block session-based information leaks and frontend detection mechanisms.
|
||||||
|
Specifically targets the web_enterprise module's subscription checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
"""
|
||||||
|
Override session info to prevent frontend from detecting license status.
|
||||||
|
This specifically blocks web_enterprise's ExpirationPanel from showing.
|
||||||
|
"""
|
||||||
|
_inherit = 'ir.http'
|
||||||
|
|
||||||
|
def session_info(self):
|
||||||
|
"""
|
||||||
|
Override session info to set permanent valid subscription data.
|
||||||
|
This prevents the frontend ExpirationPanel from showing warnings.
|
||||||
|
|
||||||
|
Key overrides:
|
||||||
|
- expiration_date: Set to far future (2099)
|
||||||
|
- expiration_reason: Set to 'renewal' (valid subscription)
|
||||||
|
- warning: Set to False to hide all warning banners
|
||||||
|
"""
|
||||||
|
result = super().session_info()
|
||||||
|
|
||||||
|
# Override expiration-related session data
|
||||||
|
# These are read by enterprise_subscription_service.js
|
||||||
|
result['expiration_date'] = '2099-12-31 23:59:59'
|
||||||
|
result['expiration_reason'] = 'renewal'
|
||||||
|
result['warning'] = False # Critical: prevents warning banners
|
||||||
|
|
||||||
|
# Remove any "already linked" subscription info
|
||||||
|
# These could trigger redirect prompts
|
||||||
|
result.pop('already_linked_subscription_url', None)
|
||||||
|
result.pop('already_linked_email', None)
|
||||||
|
result.pop('already_linked_send_mail_url', None)
|
||||||
|
|
||||||
|
_logger.debug("Session info patched - expiration set to 2099, warnings disabled")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ResUsers(models.Model):
|
||||||
|
"""
|
||||||
|
Override user creation/modification to prevent subscription checks.
|
||||||
|
When users are created, Odoo Enterprise normally contacts Odoo servers
|
||||||
|
to verify the subscription allows that many users.
|
||||||
|
"""
|
||||||
|
_inherit = 'res.users'
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""
|
||||||
|
Override create to ensure no external subscription check is triggered.
|
||||||
|
The actual check happens in publisher_warranty.contract which we've
|
||||||
|
already blocked, but this is an extra safety measure.
|
||||||
|
"""
|
||||||
|
_logger.info("Creating %d user(s) - subscription check DISABLED", len(vals_list))
|
||||||
|
|
||||||
|
# Create users normally - no external checks will happen
|
||||||
|
# because publisher_warranty.contract.update_notification is blocked
|
||||||
|
users = super().create(vals_list)
|
||||||
|
|
||||||
|
# Don't trigger any warranty checks
|
||||||
|
return users
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""
|
||||||
|
Override write to log user modifications.
|
||||||
|
"""
|
||||||
|
result = super().write(vals)
|
||||||
|
|
||||||
|
# If internal user status changed, log it
|
||||||
|
if 'share' in vals or 'groups_id' in vals:
|
||||||
|
_logger.info("User permissions updated - subscription check DISABLED")
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module intercepts clicks on external Odoo links to prevent
|
||||||
|
* referrer leakage when users click help/documentation/upgrade links.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browser } from "@web/core/browser/browser";
|
||||||
|
|
||||||
|
// Store original window.open
|
||||||
|
const originalOpen = browser.open;
|
||||||
|
|
||||||
|
// Override browser.open to add referrer protection
|
||||||
|
browser.open = function(url, target, features) {
|
||||||
|
if (url && typeof url === 'string') {
|
||||||
|
const urlLower = url.toLowerCase();
|
||||||
|
|
||||||
|
// Check if it's an Odoo external link
|
||||||
|
const odooPatterns = [
|
||||||
|
'odoo.com',
|
||||||
|
'odoo.sh',
|
||||||
|
'accounts.odoo',
|
||||||
|
];
|
||||||
|
|
||||||
|
const isOdooLink = odooPatterns.some(pattern => urlLower.includes(pattern));
|
||||||
|
|
||||||
|
if (isOdooLink) {
|
||||||
|
// For Odoo links, open with noreferrer to prevent leaking your domain
|
||||||
|
const newWindow = originalOpen.call(this, url, target || '_blank', 'noopener,noreferrer');
|
||||||
|
return newWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalOpen.call(this, url, target, features);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[disable_odoo_online] External link protection loaded');
|
||||||
|
|
||||||
220
disable_odoo_online/graphify-out/GRAPH_REPORT.md
Normal file
220
disable_odoo_online/graphify-out/GRAPH_REPORT.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# Graph Report - /Users/gurpreet/Github/Odoo-Modules/disable_odoo_online (2026-04-22)
|
||||||
|
|
||||||
|
## Corpus Check
|
||||||
|
- 22 files · ~5,870 words
|
||||||
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 106 nodes · 119 edges · 27 communities detected
|
||||||
|
- Extraction: 97% EXTRACTED · 3% INFERRED · 0% AMBIGUOUS · INFERRED: 3 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]]
|
||||||
|
|
||||||
|
## God Nodes (most connected - your core abstractions)
|
||||||
|
1. `_is_blocked_url()` - 6 edges
|
||||||
|
2. `IrConfigParameter` - 5 edges
|
||||||
|
3. `_post_init_hook()` - 4 edges
|
||||||
|
4. `IrModuleModule` - 4 edges
|
||||||
|
5. `IrCron` - 4 edges
|
||||||
|
6. `ResConfigSettings` - 4 edges
|
||||||
|
7. `PublisherWarrantyContract` - 4 edges
|
||||||
|
8. `_blocked_request()` - 4 edges
|
||||||
|
9. `_blocked_get()` - 4 edges
|
||||||
|
10. `_blocked_post()` - 4 edges
|
||||||
|
|
||||||
|
## Surprising Connections (you probably didn't know these)
|
||||||
|
- None detected - all connections are within the same source files.
|
||||||
|
|
||||||
|
## Communities
|
||||||
|
|
||||||
|
### Community 0 - "Community 0"
|
||||||
|
Cohesion: 0.14
|
||||||
|
Nodes (16): _get_message(), _get_sys_logs(), IrCron, IrHttp, IrModuleModule, _pre_dispatch(), PublisherWarrantyContract, Disable module update checks from Odoo store. (+8 more)
|
||||||
|
|
||||||
|
### Community 1 - "Community 1"
|
||||||
|
Cohesion: 0.26
|
||||||
|
Nodes (10): Base, _check_database_enterprise_expiration(), check_database_expiration(), DatabaseExpirationCheck, _get_database_expiration_date(), get_param(), IrConfigParameter, Override config parameters to prevent expiration and protect license values. (+2 more)
|
||||||
|
|
||||||
|
### Community 2 - "Community 2"
|
||||||
|
Cohesion: 0.27
|
||||||
|
Nodes (10): _blocked_get(), _blocked_post(), _blocked_request(), _is_blocked_url(), patch_requests(), Check if the URL should be blocked., Intercept and block requests to Odoo domains., Intercept and block GET requests. (+2 more)
|
||||||
|
|
||||||
|
### Community 3 - "Community 3"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (7): create(), IrHttp, Override session info to prevent frontend from detecting license status. Thi, Override session info to set permanent valid subscription data. This pre, Override user creation/modification to prevent subscription checks. When use, Override write to log user modifications., ResUsers
|
||||||
|
|
||||||
|
### Community 4 - "Community 4"
|
||||||
|
Cohesion: 0.24
|
||||||
|
Nodes (5): Override set_param to prevent external processes from changing protected values., DISABLED: Do not send any data to Odoo servers. Just update local parame, Ensure certain settings stay disabled., _post_init_hook(), Set all configuration parameters to disable external Odoo services. This run
|
||||||
|
|
||||||
|
### Community 5 - "Community 5"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (7): autocomplete(), enrich_company(), Disable partner autocomplete from Odoo API., Disable company autocomplete features., read_by_vat(), ResCompany, ResPartner
|
||||||
|
|
||||||
|
### Community 6 - "Community 6"
|
||||||
|
Cohesion: 0.4
|
||||||
|
Nodes (4): _disabled_iap_jsonrpc(), patch_iap_tools(), DISABLED: Block all IAP JSON-RPC calls. Returns empty/success response inste, Monkey-patch the iap_jsonrpc function to block external calls. This is calle
|
||||||
|
|
||||||
|
### Community 7 - "Community 7"
|
||||||
|
Cohesion: 0.53
|
||||||
|
Nodes (4): _get_gravatar_image(), _get_rates_from_provider(), ResCurrencyDisabled, ResPartnerDisabled
|
||||||
|
|
||||||
|
### Community 8 - "Community 8"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 9 - "Community 9"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 10 - "Community 10"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 11 - "Community 11"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 12 - "Community 12"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Set database to never expire.
|
||||||
|
|
||||||
|
### Community 13 - "Community 13"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Override get_param to return permanent values for protected params.
|
||||||
|
|
||||||
|
### Community 14 - "Community 14"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Override to prevent fetching from Odoo Apps store. Only scan local addon
|
||||||
|
|
||||||
|
### Community 15 - "Community 15"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Do not contact Odoo servers. Returns fake successful response.
|
||||||
|
|
||||||
|
### Community 16 - "Community 16"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return empty message.
|
||||||
|
|
||||||
|
### Community 17 - "Community 17"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Log and potentially block external service routes.
|
||||||
|
|
||||||
|
### Community 18 - "Community 18"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return empty rates.
|
||||||
|
|
||||||
|
### Community 19 - "Community 19"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return False to skip gravatar lookup.
|
||||||
|
|
||||||
|
### Community 20 - "Community 20"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return empty results instead of calling Odoo's partner API.
|
||||||
|
|
||||||
|
### Community 21 - "Community 21"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return empty data instead of calling Odoo's enrichment API.
|
||||||
|
|
||||||
|
### Community 22 - "Community 22"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return empty data instead of calling Odoo's VAT lookup API.
|
||||||
|
|
||||||
|
### Community 23 - "Community 23"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Return empty results for company autocomplete.
|
||||||
|
|
||||||
|
### Community 24 - "Community 24"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Override create to ensure no external subscription check is triggered. T
|
||||||
|
|
||||||
|
### Community 25 - "Community 25"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 26 - "Community 26"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
## Knowledge Gaps
|
||||||
|
- **39 isolated node(s):** `Set all configuration parameters to disable external Odoo services. This run`, `Override config parameters to prevent expiration and protect license values.`, `Set permanent valid subscription on module init.`, `Set database to never expire.`, `Override get_param to return permanent values for protected params.` (+34 more)
|
||||||
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
|
- **Thin community `Community 8`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 9`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 10`** (1 nodes): `__manifest__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 11`** (1 nodes): `__manifest__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 12`** (1 nodes): `Set database to never expire.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 13`** (1 nodes): `Override get_param to return permanent values for protected params.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 14`** (1 nodes): `Override to prevent fetching from Odoo Apps store. Only scan local addon`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 15`** (1 nodes): `DISABLED: Do not contact Odoo servers. Returns fake successful response.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 16`** (1 nodes): `DISABLED: Return empty message.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 17`** (1 nodes): `Log and potentially block external service routes.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 18`** (1 nodes): `DISABLED: Return empty rates.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 19`** (1 nodes): `DISABLED: Return False to skip gravatar lookup.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 20`** (1 nodes): `DISABLED: Return empty results instead of calling Odoo's partner API.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 21`** (1 nodes): `DISABLED: Return empty data instead of calling Odoo's enrichment API.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 22`** (1 nodes): `DISABLED: Return empty data instead of calling Odoo's VAT lookup API.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 23`** (1 nodes): `DISABLED: Return empty results for company autocomplete.`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 24`** (1 nodes): `Override create to ensure no external subscription check is triggered. T`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 25`** (1 nodes): `disable_external_links.js`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 26`** (1 nodes): `disable_external_links.js`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
|
||||||
|
## Suggested Questions
|
||||||
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
|
- **Why does `IrConfigParameter` connect `Community 1` to `Community 4`?**
|
||||||
|
_High betweenness centrality (0.069) - this node is a cross-community bridge._
|
||||||
|
- **Why does `ResConfigSettings` connect `Community 0` to `Community 4`?**
|
||||||
|
_High betweenness centrality (0.042) - this node is a cross-community bridge._
|
||||||
|
- **Why does `PublisherWarrantyContract` connect `Community 0` to `Community 4`?**
|
||||||
|
_High betweenness centrality (0.042) - this node is a cross-community bridge._
|
||||||
|
- **What connects `Set all configuration parameters to disable external Odoo services. This run`, `Override config parameters to prevent expiration and protect license values.`, `Set permanent valid subscription on module init.` to the rest of the system?**
|
||||||
|
_39 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
|
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||||
|
_Cohesion score 0.14 - nodes in this community are weakly interconnected._
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "label": "disable_all_external.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L1"}, {"id": "disable_all_external_rescurrencydisabled", "label": "ResCurrencyDisabled", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L17"}, {"id": "disable_all_external_get_rates_from_provider", "label": "_get_rates_from_provider()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L21"}, {"id": "disable_all_external_respartnerdisabled", "label": "ResPartnerDisabled", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L31"}, {"id": "disable_all_external_get_gravatar_image", "label": "_get_gravatar_image()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L35"}, {"id": "disable_all_external_rationale_22", "label": "DISABLED: Return empty rates.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L22"}, {"id": "disable_all_external_rationale_36", "label": "DISABLED: Return False to skip gravatar lookup.", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L36"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "target": "disable_all_external_rescurrencydisabled", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L17", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "target": "disable_all_external_get_rates_from_provider", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L21", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "target": "disable_all_external_respartnerdisabled", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L31", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_all_external_py", "target": "disable_all_external_get_gravatar_image", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L35", "weight": 1.0}, {"source": "disable_all_external_rationale_22", "target": "disable_all_external_rescurrencydisabled_get_rates_from_provider", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L22", "weight": 1.0}, {"source": "disable_all_external_rationale_36", "target": "disable_all_external_respartnerdisabled_get_gravatar_image", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L36", "weight": 1.0}], "raw_calls": [{"caller_nid": "disable_all_external_get_rates_from_provider", "callee": "debug", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L23"}, {"caller_nid": "disable_all_external_get_gravatar_image", "callee": "debug", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_all_external.py", "source_location": "L37"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_static_src_js_disable_external_links_js", "label": "disable_external_links.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/static/src/js/disable_external_links.js", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_static_src_js_disable_external_links_js", "target": "browser", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/static/src/js/disable_external_links.js", "source_location": "L8", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/__init__.py", "source_location": "L8", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_static_src_js_disable_external_links_js", "label": "disable_external_links.js", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/static/src/js/disable_external_links.js", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_static_src_js_disable_external_links_js", "target": "browser", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/static/src/js/disable_external_links.js", "source_location": "L8", "weight": 1.0}], "raw_calls": []}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L3", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L4", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L6", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/__init__.py", "source_location": "L8", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_disable_iap_tools_py", "label": "disable_iap_tools.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L1"}, {"id": "disable_iap_tools_disabled_iap_jsonrpc", "label": "_disabled_iap_jsonrpc()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L16"}, {"id": "disable_iap_tools_patch_iap_tools", "label": "patch_iap_tools()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L43"}, {"id": "disable_iap_tools_rationale_17", "label": "DISABLED: Block all IAP JSON-RPC calls. Returns empty/success response inste", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L17"}, {"id": "disable_iap_tools_rationale_44", "label": "Monkey-patch the iap_jsonrpc function to block external calls. This is calle", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L44"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_disable_iap_tools_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_disable_iap_tools_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_disable_iap_tools_py", "target": "disable_iap_tools_disabled_iap_jsonrpc", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L16", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_models_disable_iap_tools_py", "target": "disable_iap_tools_patch_iap_tools", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L43", "weight": 1.0}, {"source": "disable_iap_tools_rationale_17", "target": "disable_iap_tools_disabled_iap_jsonrpc", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L17", "weight": 1.0}, {"source": "disable_iap_tools_rationale_44", "target": "disable_iap_tools_patch_iap_tools", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L44", "weight": 1.0}], "raw_calls": [{"caller_nid": "disable_iap_tools_disabled_iap_jsonrpc", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L21"}, {"caller_nid": "disable_iap_tools_disabled_iap_jsonrpc", "callee": "warning", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L35"}, {"caller_nid": "disable_iap_tools_patch_iap_tools", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L57"}, {"caller_nid": "disable_iap_tools_patch_iap_tools", "callee": "debug", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L60"}, {"caller_nid": "disable_iap_tools_patch_iap_tools", "callee": "warning", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L62"}]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L1"}, {"id": "init_post_init_hook", "label": "_post_init_hook()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L5"}, {"id": "init_rationale_6", "label": "Set all configuration parameters to disable external Odoo services. This run", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L6"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_init_py", "target": "init_post_init_hook", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "init_rationale_6", "target": "init_post_init_hook", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "init_post_init_hook", "callee": "getLogger", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L11"}, {"caller_nid": "init_post_init_hook", "callee": "sudo", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L13"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L54"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L55"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L56"}, {"caller_nid": "init_post_init_hook", "callee": "items", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L58"}, {"caller_nid": "init_post_init_hook", "callee": "set_param", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L60"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L61"}, {"caller_nid": "init_post_init_hook", "callee": "len", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L61"}, {"caller_nid": "init_post_init_hook", "callee": "str", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L61"}, {"caller_nid": "init_post_init_hook", "callee": "warning", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L63"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L65"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L66"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__init__.py", "source_location": "L67"}]}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L1"}, {"id": "init_post_init_hook", "label": "_post_init_hook()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L5"}, {"id": "init_rationale_6", "label": "Set all configuration parameters to disable external Odoo services. This run", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L6"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_init_py", "target": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L2", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_disable_odoo_online_init_py", "target": "init_post_init_hook", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L5", "weight": 1.0}, {"source": "init_rationale_6", "target": "init_post_init_hook", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "init_post_init_hook", "callee": "getLogger", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L11"}, {"caller_nid": "init_post_init_hook", "callee": "sudo", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L13"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L54"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L55"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L56"}, {"caller_nid": "init_post_init_hook", "callee": "items", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L58"}, {"caller_nid": "init_post_init_hook", "callee": "set_param", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L60"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L61"}, {"caller_nid": "init_post_init_hook", "callee": "len", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L61"}, {"caller_nid": "init_post_init_hook", "callee": "str", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L61"}, {"caller_nid": "init_post_init_hook", "callee": "warning", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L63"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L65"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L66"}, {"caller_nid": "init_post_init_hook", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/disable_odoo_online/__init__.py", "source_location": "L67"}]}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_iap_tools_py", "label": "disable_iap_tools.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L1"}, {"id": "disable_iap_tools_disabled_iap_jsonrpc", "label": "_disabled_iap_jsonrpc()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L16"}, {"id": "disable_iap_tools_patch_iap_tools", "label": "patch_iap_tools()", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L43"}, {"id": "disable_iap_tools_rationale_17", "label": "DISABLED: Block all IAP JSON-RPC calls. Returns empty/success response inste", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L17"}, {"id": "disable_iap_tools_rationale_44", "label": "Monkey-patch the iap_jsonrpc function to block external calls. This is calle", "file_type": "rationale", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L44"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_iap_tools_py", "target": "logging", "relation": "imports", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L7", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_iap_tools_py", "target": "odoo", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L8", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_iap_tools_py", "target": "disable_iap_tools_disabled_iap_jsonrpc", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L16", "weight": 1.0}, {"source": "users_gurpreet_github_odoo_modules_disable_odoo_online_models_disable_iap_tools_py", "target": "disable_iap_tools_patch_iap_tools", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L43", "weight": 1.0}, {"source": "disable_iap_tools_rationale_17", "target": "disable_iap_tools_disabled_iap_jsonrpc", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L17", "weight": 1.0}, {"source": "disable_iap_tools_rationale_44", "target": "disable_iap_tools_patch_iap_tools", "relation": "rationale_for", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L44", "weight": 1.0}], "raw_calls": [{"caller_nid": "disable_iap_tools_disabled_iap_jsonrpc", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L21"}, {"caller_nid": "disable_iap_tools_disabled_iap_jsonrpc", "callee": "warning", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L35"}, {"caller_nid": "disable_iap_tools_patch_iap_tools", "callee": "info", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L57"}, {"caller_nid": "disable_iap_tools_patch_iap_tools", "callee": "debug", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L60"}, {"caller_nid": "disable_iap_tools_patch_iap_tools", "callee": "warning", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_odoo_online/models/disable_iap_tools.py", "source_location": "L62"}]}
|
||||||
File diff suppressed because one or more lines are too long
257
disable_odoo_online/graphify-out/graph.html
Normal file
257
disable_odoo_online/graphify-out/graph.html
Normal file
File diff suppressed because one or more lines are too long
2392
disable_odoo_online/graphify-out/graph.json
Normal file
2392
disable_odoo_online/graphify-out/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
8
disable_odoo_online/models/__init__.py
Normal file
8
disable_odoo_online/models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import disable_iap_tools # Patches iap_jsonrpc globally - MUST be first
|
||||||
|
from . import disable_http_requests # Patches requests library to block Odoo domains
|
||||||
|
from . import disable_online_services
|
||||||
|
from . import disable_partner_autocomplete
|
||||||
|
from . import disable_database_expiration
|
||||||
|
from . import disable_all_external
|
||||||
|
from . import disable_session_leaks
|
||||||
38
disable_odoo_online/models/disable_all_external.py
Normal file
38
disable_odoo_online/models/disable_all_external.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Comprehensive blocking of ALL external Odoo service calls.
|
||||||
|
Only inherits from models that are guaranteed to exist in base Odoo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Block Currency Rate Live Updates - Uses res.currency which always exists
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class ResCurrencyDisabled(models.Model):
|
||||||
|
_inherit = 'res.currency'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_rates_from_provider(self, provider, date):
|
||||||
|
"""DISABLED: Return empty rates."""
|
||||||
|
_logger.debug("Currency rate provider BLOCKED: provider=%s", provider)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Block Gravatar - Uses res.partner which always exists
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class ResPartnerDisabled(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_gravatar_image(self, email):
|
||||||
|
"""DISABLED: Return False to skip gravatar lookup."""
|
||||||
|
_logger.debug("Gravatar lookup BLOCKED for email=%s", email)
|
||||||
|
return False
|
||||||
106
disable_odoo_online/models/disable_database_expiration.py
Normal file
106
disable_odoo_online/models/disable_database_expiration.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Disable database expiration checks and registration.
|
||||||
|
Consolidates all ir.config_parameter overrides.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrConfigParameter(models.Model):
|
||||||
|
"""Override config parameters to prevent expiration and protect license values."""
|
||||||
|
_inherit = 'ir.config_parameter'
|
||||||
|
|
||||||
|
PROTECTED_PARAMS = {
|
||||||
|
'database.expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'database.expiration_reason': 'renewal',
|
||||||
|
'database.enterprise_code': 'PERMANENT_LOCAL',
|
||||||
|
}
|
||||||
|
|
||||||
|
CLEAR_PARAMS = [
|
||||||
|
'database.already_linked_subscription_url',
|
||||||
|
'database.already_linked_email',
|
||||||
|
'database.already_linked_send_mail_url',
|
||||||
|
]
|
||||||
|
|
||||||
|
def init(self, force=False):
|
||||||
|
"""Set permanent valid subscription on module init."""
|
||||||
|
super().init(force=force)
|
||||||
|
self._set_permanent_subscription()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _set_permanent_subscription(self):
|
||||||
|
"""Set database to never expire."""
|
||||||
|
_logger.info("Setting permanent subscription values...")
|
||||||
|
|
||||||
|
for key, value in self.PROTECTED_PARAMS.items():
|
||||||
|
try:
|
||||||
|
self.env.cr.execute("""
|
||||||
|
INSERT INTO ir_config_parameter (key, value, create_uid, create_date, write_uid, write_date)
|
||||||
|
VALUES (%s, %s, %s, NOW() AT TIME ZONE 'UTC', %s, NOW() AT TIME ZONE 'UTC')
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = %s, write_date = NOW() AT TIME ZONE 'UTC'
|
||||||
|
""", (key, value, self.env.uid, self.env.uid, value))
|
||||||
|
except Exception as e:
|
||||||
|
_logger.debug("Could not set param %s: %s", key, e)
|
||||||
|
|
||||||
|
for key in self.CLEAR_PARAMS:
|
||||||
|
try:
|
||||||
|
self.env.cr.execute("""
|
||||||
|
INSERT INTO ir_config_parameter (key, value, create_uid, create_date, write_uid, write_date)
|
||||||
|
VALUES (%s, '', %s, NOW() AT TIME ZONE 'UTC', %s, NOW() AT TIME ZONE 'UTC')
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = '', write_date = NOW() AT TIME ZONE 'UTC'
|
||||||
|
""", (key, self.env.uid, self.env.uid))
|
||||||
|
except Exception as e:
|
||||||
|
_logger.debug("Could not clear param %s: %s", key, e)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_param(self, key, default=False):
|
||||||
|
"""Override get_param to return permanent values for protected params."""
|
||||||
|
if key in self.PROTECTED_PARAMS:
|
||||||
|
return self.PROTECTED_PARAMS[key]
|
||||||
|
|
||||||
|
if key in self.CLEAR_PARAMS:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return super().get_param(key, default)
|
||||||
|
|
||||||
|
def set_param(self, key, value):
|
||||||
|
"""Override set_param to prevent external processes from changing protected values."""
|
||||||
|
if key in self.PROTECTED_PARAMS:
|
||||||
|
if value != self.PROTECTED_PARAMS[key]:
|
||||||
|
_logger.warning("Blocked attempt to change protected param %s to %s", key, value)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if key in self.CLEAR_PARAMS:
|
||||||
|
value = ''
|
||||||
|
|
||||||
|
return super().set_param(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseExpirationCheck(models.AbstractModel):
|
||||||
|
_name = 'disable.odoo.online.expiration'
|
||||||
|
_description = 'Database Expiration Blocker'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def check_database_expiration(self):
|
||||||
|
return {
|
||||||
|
'valid': True,
|
||||||
|
'expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'expiration_reason': 'renewal',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Base(models.AbstractModel):
|
||||||
|
_inherit = 'base'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_database_expiration_date(self):
|
||||||
|
return datetime(2099, 12, 31, 23, 59, 59)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _check_database_enterprise_expiration(self):
|
||||||
|
return True
|
||||||
129
disable_odoo_online/models/disable_http_requests.py
Normal file
129
disable_odoo_online/models/disable_http_requests.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Block ALL outgoing HTTP requests to Odoo-related domains.
|
||||||
|
This patches the requests library to intercept and block external calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from functools import wraps
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Domains to block - all Odoo external services
|
||||||
|
BLOCKED_DOMAINS = [
|
||||||
|
'odoo.com',
|
||||||
|
'odoofin.com',
|
||||||
|
'odoo.sh',
|
||||||
|
'iap.odoo.com',
|
||||||
|
'iap-services.odoo.com',
|
||||||
|
'partner-autocomplete.odoo.com',
|
||||||
|
'iap-extract.odoo.com',
|
||||||
|
'iap-sms.odoo.com',
|
||||||
|
'upgrade.odoo.com',
|
||||||
|
'apps.odoo.com',
|
||||||
|
'production.odoofin.com',
|
||||||
|
'plaid.com',
|
||||||
|
'yodlee.com',
|
||||||
|
'gravatar.com',
|
||||||
|
'www.gravatar.com',
|
||||||
|
'secure.gravatar.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Store original functions
|
||||||
|
_original_request = None
|
||||||
|
_original_get = None
|
||||||
|
_original_post = None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_blocked_url(url):
|
||||||
|
"""Check if the URL should be blocked."""
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
domain = parsed.netloc.lower()
|
||||||
|
for blocked in BLOCKED_DOMAINS:
|
||||||
|
if blocked in domain:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_request(method, url, **kwargs):
|
||||||
|
"""Intercept and block requests to Odoo domains."""
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP REQUEST BLOCKED: %s %s", method.upper(), url)
|
||||||
|
# Return a mock response
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
return response
|
||||||
|
return _original_request(method, url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_get(url, **kwargs):
|
||||||
|
"""Intercept and block GET requests."""
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP GET BLOCKED: %s", url)
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
return response
|
||||||
|
return _original_get(url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _blocked_post(url, **kwargs):
|
||||||
|
"""Intercept and block POST requests."""
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP POST BLOCKED: %s", url)
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
return response
|
||||||
|
return _original_post(url, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_requests():
|
||||||
|
"""Monkey-patch requests library to block Odoo domains."""
|
||||||
|
global _original_request, _original_get, _original_post
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _original_request is None:
|
||||||
|
_original_request = requests.Session.request
|
||||||
|
_original_get = requests.get
|
||||||
|
_original_post = requests.post
|
||||||
|
|
||||||
|
# Patch Session.request (catches most calls)
|
||||||
|
def patched_session_request(self, method, url, **kwargs):
|
||||||
|
if _is_blocked_url(url):
|
||||||
|
_logger.warning("HTTP SESSION REQUEST BLOCKED: %s %s", method.upper(), url)
|
||||||
|
response = requests.models.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b'{}'
|
||||||
|
response.headers['Content-Type'] = 'application/json'
|
||||||
|
response.request = requests.models.PreparedRequest()
|
||||||
|
response.request.url = url
|
||||||
|
response.request.method = method
|
||||||
|
return response
|
||||||
|
return _original_request(self, method, url, **kwargs)
|
||||||
|
|
||||||
|
requests.Session.request = patched_session_request
|
||||||
|
requests.get = _blocked_get
|
||||||
|
requests.post = _blocked_post
|
||||||
|
|
||||||
|
_logger.info("HTTP requests to Odoo domains have been BLOCKED")
|
||||||
|
_logger.info("Blocked domains: %s", ', '.join(BLOCKED_DOMAINS))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not patch requests library: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# Apply patch when module is imported
|
||||||
|
patch_requests()
|
||||||
|
|
||||||
67
disable_odoo_online/models/disable_iap_tools.py
Normal file
67
disable_odoo_online/models/disable_iap_tools.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Override the core IAP tools to block ALL external API calls.
|
||||||
|
This is the master switch that blocks ALL Odoo external communications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import exceptions, _
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Store original function reference
|
||||||
|
_original_iap_jsonrpc = None
|
||||||
|
|
||||||
|
|
||||||
|
def _disabled_iap_jsonrpc(url, method='call', params=None, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Block all IAP JSON-RPC calls.
|
||||||
|
Returns empty/success response instead of making external calls.
|
||||||
|
"""
|
||||||
|
_logger.info("IAP JSONRPC BLOCKED: %s (method=%s)", url, method)
|
||||||
|
|
||||||
|
# Return appropriate empty responses based on the endpoint
|
||||||
|
if '/authorize' in url:
|
||||||
|
return 'fake_transaction_token_disabled'
|
||||||
|
elif '/capture' in url or '/cancel' in url:
|
||||||
|
return True
|
||||||
|
elif '/credits' in url:
|
||||||
|
return 999999
|
||||||
|
elif 'partner-autocomplete' in url:
|
||||||
|
return []
|
||||||
|
elif 'enrich' in url:
|
||||||
|
return {}
|
||||||
|
elif 'sms' in url:
|
||||||
|
_logger.warning("SMS API call blocked - SMS will not be sent")
|
||||||
|
return {'state': 'success', 'credits': 999999}
|
||||||
|
elif 'extract' in url:
|
||||||
|
return {'status': 'success', 'credits': 999999}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def patch_iap_tools():
|
||||||
|
"""
|
||||||
|
Monkey-patch the iap_jsonrpc function to block external calls.
|
||||||
|
This is called when the module loads.
|
||||||
|
"""
|
||||||
|
global _original_iap_jsonrpc
|
||||||
|
|
||||||
|
try:
|
||||||
|
from odoo.addons.iap.tools import iap_tools
|
||||||
|
|
||||||
|
if _original_iap_jsonrpc is None:
|
||||||
|
_original_iap_jsonrpc = iap_tools.iap_jsonrpc
|
||||||
|
|
||||||
|
iap_tools.iap_jsonrpc = _disabled_iap_jsonrpc
|
||||||
|
_logger.info("IAP JSON-RPC calls have been DISABLED globally")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
_logger.debug("IAP module not installed, skipping patch")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not patch IAP tools: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
# Apply patch when module is imported
|
||||||
|
patch_iap_tools()
|
||||||
|
|
||||||
153
disable_odoo_online/models/disable_online_services.py
Normal file
153
disable_odoo_online/models/disable_online_services.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Disable various Odoo online services and external API calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrModuleModule(models.Model):
|
||||||
|
"""Disable module update checks from Odoo store."""
|
||||||
|
_inherit = 'ir.module.module'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def update_list(self):
|
||||||
|
"""
|
||||||
|
Override to prevent fetching from Odoo Apps store.
|
||||||
|
Only scan local addons paths.
|
||||||
|
"""
|
||||||
|
_logger.info("Module update_list: Scanning local addons only (Odoo Apps store disabled)")
|
||||||
|
return super().update_list()
|
||||||
|
|
||||||
|
def button_immediate_upgrade(self):
|
||||||
|
"""Prevent upgrade attempts that might contact Odoo."""
|
||||||
|
_logger.info("Module upgrade: Processing locally only")
|
||||||
|
return super().button_immediate_upgrade()
|
||||||
|
|
||||||
|
|
||||||
|
class IrCron(models.Model):
|
||||||
|
"""Disable scheduled actions that contact Odoo servers."""
|
||||||
|
_inherit = 'ir.cron'
|
||||||
|
|
||||||
|
def _callback(self, cron_name, server_action_id):
|
||||||
|
"""
|
||||||
|
Override to block certain cron jobs that contact Odoo.
|
||||||
|
Odoo 19 signature: _callback(self, cron_name, server_action_id)
|
||||||
|
"""
|
||||||
|
blocked_crons = [
|
||||||
|
'publisher',
|
||||||
|
'warranty',
|
||||||
|
'update_notification',
|
||||||
|
'database_expiration',
|
||||||
|
'iap_enrich',
|
||||||
|
'ocr',
|
||||||
|
'Invoice OCR',
|
||||||
|
'enrich leads',
|
||||||
|
'fetchmail',
|
||||||
|
'online sync',
|
||||||
|
]
|
||||||
|
|
||||||
|
cron_lower = (cron_name or '').lower()
|
||||||
|
for blocked in blocked_crons:
|
||||||
|
if blocked.lower() in cron_lower:
|
||||||
|
_logger.info("Cron BLOCKED (external call): %s", cron_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return super()._callback(cron_name, server_action_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
"""Override config settings to prevent external service configuration."""
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
def set_values(self):
|
||||||
|
"""Ensure certain settings stay disabled."""
|
||||||
|
res = super().set_values()
|
||||||
|
|
||||||
|
# Disable any auto-update settings and set permanent expiration
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
params.set_param('database.expiration_date', '2099-12-31 23:59:59')
|
||||||
|
params.set_param('database.expiration_reason', 'renewal')
|
||||||
|
params.set_param('database.enterprise_code', 'PERMANENT_LOCAL')
|
||||||
|
|
||||||
|
# Disable IAP endpoint (redirect to nowhere)
|
||||||
|
params.set_param('iap.endpoint', 'http://localhost:65535')
|
||||||
|
|
||||||
|
# Disable various external services
|
||||||
|
params.set_param('partner_autocomplete.endpoint', 'http://localhost:65535')
|
||||||
|
params.set_param('iap_extract_endpoint', 'http://localhost:65535')
|
||||||
|
params.set_param('olg.endpoint', 'http://localhost:65535')
|
||||||
|
params.set_param('mail.media_library_endpoint', 'http://localhost:65535')
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class PublisherWarrantyContract(models.AbstractModel):
|
||||||
|
"""Completely disable publisher warranty checks."""
|
||||||
|
_inherit = 'publisher_warranty.contract'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_sys_logs(self):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not contact Odoo servers.
|
||||||
|
Returns fake successful response.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty _get_sys_logs BLOCKED")
|
||||||
|
return {
|
||||||
|
'messages': [],
|
||||||
|
'enterprise_info': {
|
||||||
|
'expiration_date': '2099-12-31 23:59:59',
|
||||||
|
'expiration_reason': 'renewal',
|
||||||
|
'enterprise_code': 'PERMANENT_LOCAL',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_message(self):
|
||||||
|
"""DISABLED: Return empty message."""
|
||||||
|
_logger.info("Publisher warranty _get_message BLOCKED")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_notification(self, cron_mode=True):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not send any data to Odoo servers.
|
||||||
|
Just update local parameters with permanent values.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty update_notification BLOCKED")
|
||||||
|
|
||||||
|
# Set permanent valid subscription parameters
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
params.set_param('database.expiration_date', '2099-12-31 23:59:59')
|
||||||
|
params.set_param('database.expiration_reason', 'renewal')
|
||||||
|
params.set_param('database.enterprise_code', 'PERMANENT_LOCAL')
|
||||||
|
|
||||||
|
# Clear any "already linked" parameters
|
||||||
|
params.set_param('database.already_linked_subscription_url', '')
|
||||||
|
params.set_param('database.already_linked_email', '')
|
||||||
|
params.set_param('database.already_linked_send_mail_url', '')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
"""Block certain routes that call external services."""
|
||||||
|
_inherit = 'ir.http'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pre_dispatch(cls, rule, arguments):
|
||||||
|
"""Log and potentially block external service routes."""
|
||||||
|
# List of route patterns that should be blocked
|
||||||
|
blocked_routes = [
|
||||||
|
'/iap/',
|
||||||
|
'/partner_autocomplete/',
|
||||||
|
'/google_',
|
||||||
|
'/ocr/',
|
||||||
|
'/sms/',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Note: We don't actually block here as it might break functionality
|
||||||
|
# The actual blocking happens at the API/model level
|
||||||
|
return super()._pre_dispatch(rule, arguments)
|
||||||
52
disable_odoo_online/models/disable_partner_autocomplete.py
Normal file
52
disable_odoo_online/models/disable_partner_autocomplete.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Disable Partner Autocomplete external API calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
"""Disable partner autocomplete from Odoo API."""
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def autocomplete(self, query, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty results instead of calling Odoo's partner API.
|
||||||
|
"""
|
||||||
|
_logger.debug("Partner autocomplete DISABLED - returning empty results for: %s", query)
|
||||||
|
return []
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def enrich_company(self, company_domain, partner_gid, vat, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty data instead of calling Odoo's enrichment API.
|
||||||
|
"""
|
||||||
|
_logger.debug("Partner enrichment DISABLED - returning empty for domain: %s", company_domain)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def read_by_vat(self, vat, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty data instead of calling Odoo's VAT lookup API.
|
||||||
|
"""
|
||||||
|
_logger.debug("Partner VAT lookup DISABLED - returning empty for VAT: %s", vat)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
"""Disable company autocomplete features."""
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def autocomplete(self, query, timeout=15):
|
||||||
|
"""
|
||||||
|
DISABLED: Return empty results for company autocomplete.
|
||||||
|
"""
|
||||||
|
_logger.debug("Company autocomplete DISABLED - returning empty results")
|
||||||
|
return []
|
||||||
|
|
||||||
82
disable_odoo_online/models/disable_session_leaks.py
Normal file
82
disable_odoo_online/models/disable_session_leaks.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Block session-based information leaks and frontend detection mechanisms.
|
||||||
|
Specifically targets the web_enterprise module's subscription checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
"""
|
||||||
|
Override session info to prevent frontend from detecting license status.
|
||||||
|
This specifically blocks web_enterprise's ExpirationPanel from showing.
|
||||||
|
"""
|
||||||
|
_inherit = 'ir.http'
|
||||||
|
|
||||||
|
def session_info(self):
|
||||||
|
"""
|
||||||
|
Override session info to set permanent valid subscription data.
|
||||||
|
This prevents the frontend ExpirationPanel from showing warnings.
|
||||||
|
|
||||||
|
Key overrides:
|
||||||
|
- expiration_date: Set to far future (2099)
|
||||||
|
- expiration_reason: Set to 'renewal' (valid subscription)
|
||||||
|
- warning: Set to False to hide all warning banners
|
||||||
|
"""
|
||||||
|
result = super().session_info()
|
||||||
|
|
||||||
|
# Override expiration-related session data
|
||||||
|
# These are read by enterprise_subscription_service.js
|
||||||
|
result['expiration_date'] = '2099-12-31 23:59:59'
|
||||||
|
result['expiration_reason'] = 'renewal'
|
||||||
|
result['warning'] = False # Critical: prevents warning banners
|
||||||
|
|
||||||
|
# Remove any "already linked" subscription info
|
||||||
|
# These could trigger redirect prompts
|
||||||
|
result.pop('already_linked_subscription_url', None)
|
||||||
|
result.pop('already_linked_email', None)
|
||||||
|
result.pop('already_linked_send_mail_url', None)
|
||||||
|
|
||||||
|
_logger.debug("Session info patched - expiration set to 2099, warnings disabled")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ResUsers(models.Model):
|
||||||
|
"""
|
||||||
|
Override user creation/modification to prevent subscription checks.
|
||||||
|
When users are created, Odoo Enterprise normally contacts Odoo servers
|
||||||
|
to verify the subscription allows that many users.
|
||||||
|
"""
|
||||||
|
_inherit = 'res.users'
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""
|
||||||
|
Override create to ensure no external subscription check is triggered.
|
||||||
|
The actual check happens in publisher_warranty.contract which we've
|
||||||
|
already blocked, but this is an extra safety measure.
|
||||||
|
"""
|
||||||
|
_logger.info("Creating %d user(s) - subscription check DISABLED", len(vals_list))
|
||||||
|
|
||||||
|
# Create users normally - no external checks will happen
|
||||||
|
# because publisher_warranty.contract.update_notification is blocked
|
||||||
|
users = super().create(vals_list)
|
||||||
|
|
||||||
|
# Don't trigger any warranty checks
|
||||||
|
return users
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""
|
||||||
|
Override write to log user modifications.
|
||||||
|
"""
|
||||||
|
result = super().write(vals)
|
||||||
|
|
||||||
|
# If internal user status changed, log it
|
||||||
|
if 'share' in vals or 'groups_id' in vals:
|
||||||
|
_logger.info("User permissions updated - subscription check DISABLED")
|
||||||
|
|
||||||
|
return result
|
||||||
38
disable_odoo_online/static/src/js/disable_external_links.js
Normal file
38
disable_odoo_online/static/src/js/disable_external_links.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module intercepts clicks on external Odoo links to prevent
|
||||||
|
* referrer leakage when users click help/documentation/upgrade links.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browser } from "@web/core/browser/browser";
|
||||||
|
|
||||||
|
// Store original window.open
|
||||||
|
const originalOpen = browser.open;
|
||||||
|
|
||||||
|
// Override browser.open to add referrer protection
|
||||||
|
browser.open = function(url, target, features) {
|
||||||
|
if (url && typeof url === 'string') {
|
||||||
|
const urlLower = url.toLowerCase();
|
||||||
|
|
||||||
|
// Check if it's an Odoo external link
|
||||||
|
const odooPatterns = [
|
||||||
|
'odoo.com',
|
||||||
|
'odoo.sh',
|
||||||
|
'accounts.odoo',
|
||||||
|
];
|
||||||
|
|
||||||
|
const isOdooLink = odooPatterns.some(pattern => urlLower.includes(pattern));
|
||||||
|
|
||||||
|
if (isOdooLink) {
|
||||||
|
// For Odoo links, open with noreferrer to prevent leaking your domain
|
||||||
|
const newWindow = originalOpen.call(this, url, target || '_blank', 'noopener,noreferrer');
|
||||||
|
return newWindow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalOpen.call(this, url, target, features);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[disable_odoo_online] External link protection loaded');
|
||||||
|
|
||||||
3
disable_publisher_warranty/__init__.py
Normal file
3
disable_publisher_warranty/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
||||||
22
disable_publisher_warranty/__manifest__.py
Normal file
22
disable_publisher_warranty/__manifest__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Disable Publisher Warranty',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Tools',
|
||||||
|
'summary': 'Disables all communication with Odoo publisher warranty servers',
|
||||||
|
'description': """
|
||||||
|
This module completely disables:
|
||||||
|
- Publisher warranty server communication
|
||||||
|
- Subscription expiration checks
|
||||||
|
- Automatic license updates
|
||||||
|
|
||||||
|
For local development use only.
|
||||||
|
""",
|
||||||
|
'author': 'Development',
|
||||||
|
'depends': ['mail'],
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Disable Publisher Warranty',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Tools',
|
||||||
|
'summary': 'Disables all communication with Odoo publisher warranty servers',
|
||||||
|
'description': """
|
||||||
|
This module completely disables:
|
||||||
|
- Publisher warranty server communication
|
||||||
|
- Subscription expiration checks
|
||||||
|
- Automatic license updates
|
||||||
|
|
||||||
|
For local development use only.
|
||||||
|
""",
|
||||||
|
'author': 'Development',
|
||||||
|
'depends': ['mail'],
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import publisher_warranty
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Disable all publisher warranty / subscription checks for local development
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PublisherWarrantyContractDisabled(models.AbstractModel):
|
||||||
|
_inherit = "publisher_warranty.contract"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_sys_logs(self):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not contact Odoo servers.
|
||||||
|
Returns fake successful response.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty check DISABLED - not contacting Odoo servers")
|
||||||
|
return {
|
||||||
|
"messages": [],
|
||||||
|
"enterprise_info": {
|
||||||
|
"expiration_date": "2099-12-31 23:59:59",
|
||||||
|
"expiration_reason": "renewal",
|
||||||
|
"enterprise_code": self.env['ir.config_parameter'].sudo().get_param('database.enterprise_code', ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_notification(self, cron_mode=True):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not send any data to Odoo servers.
|
||||||
|
Just update local parameters with permanent values.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty update_notification DISABLED - no server contact")
|
||||||
|
|
||||||
|
# Set permanent valid subscription parameters
|
||||||
|
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||||
|
set_param('database.expiration_date', '2099-12-31 23:59:59')
|
||||||
|
set_param('database.expiration_reason', 'renewal')
|
||||||
|
|
||||||
|
# Clear any "already linked" parameters
|
||||||
|
set_param('database.already_linked_subscription_url', False)
|
||||||
|
set_param('database.already_linked_email', False)
|
||||||
|
set_param('database.already_linked_send_mail_url', False)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
96
disable_publisher_warranty/graphify-out/GRAPH_REPORT.md
Normal file
96
disable_publisher_warranty/graphify-out/GRAPH_REPORT.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Graph Report - /Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty (2026-04-22)
|
||||||
|
|
||||||
|
## Corpus Check
|
||||||
|
- 8 files · ~414 words
|
||||||
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 13 nodes · 10 edges · 9 communities detected
|
||||||
|
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS
|
||||||
|
- 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]]
|
||||||
|
|
||||||
|
## God Nodes (most connected - your core abstractions)
|
||||||
|
1. `PublisherWarrantyContractDisabled` - 3 edges
|
||||||
|
2. `_get_sys_logs()` - 2 edges
|
||||||
|
3. `DISABLED: Do not send any data to Odoo servers. Just update local parame` - 1 edges
|
||||||
|
4. `DISABLED: Do not contact Odoo servers. Returns fake successful response.` - 0 edges
|
||||||
|
|
||||||
|
## Surprising Connections (you probably didn't know these)
|
||||||
|
- None detected - all connections are within the same source files.
|
||||||
|
|
||||||
|
## Communities
|
||||||
|
|
||||||
|
### Community 0 - "Community 0"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): _get_sys_logs(), PublisherWarrantyContractDisabled
|
||||||
|
|
||||||
|
### Community 1 - "Community 1"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Do not send any data to Odoo servers. Just update local parame
|
||||||
|
|
||||||
|
### Community 2 - "Community 2"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 3 - "Community 3"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 4 - "Community 4"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 5 - "Community 5"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 6 - "Community 6"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 7 - "Community 7"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (0):
|
||||||
|
|
||||||
|
### Community 8 - "Community 8"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): DISABLED: Do not contact Odoo servers. Returns fake successful response.
|
||||||
|
|
||||||
|
## Knowledge Gaps
|
||||||
|
- **2 isolated node(s):** `DISABLED: Do not contact Odoo servers. Returns fake successful response.`, `DISABLED: Do not send any data to Odoo servers. Just update local parame`
|
||||||
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
|
- **Thin community `Community 1`** (2 nodes): `.update_notification()`, `DISABLED: Do not send any data to Odoo servers. Just update local parame`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 2`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 3`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 4`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 5`** (1 nodes): `__init__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 6`** (1 nodes): `__manifest__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 7`** (1 nodes): `__manifest__.py`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 8`** (1 nodes): `DISABLED: Do not contact Odoo servers. Returns fake successful response.`
|
||||||
|
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 `PublisherWarrantyContractDisabled` connect `Community 0` to `Community 1`?**
|
||||||
|
_High betweenness centrality (0.098) - this node is a cross-community bridge._
|
||||||
|
- **What connects `DISABLED: Do not contact Odoo servers. Returns fake successful response.`, `DISABLED: Do not send any data to Odoo servers. Just update local parame` to the rest of the system?**
|
||||||
|
_2 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py", "target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py", "target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py", "label": "__init__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/__init__.py", "source_location": "L1"}], "edges": [{"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py", "target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/__init__.py", "source_location": "L2", "weight": 1.0}], "raw_calls": []}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_manifest_py", "label": "__manifest__.py", "file_type": "code", "source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/__manifest__.py", "source_location": "L1"}], "edges": [], "raw_calls": []}
|
||||||
257
disable_publisher_warranty/graphify-out/graph.html
Normal file
257
disable_publisher_warranty/graphify-out/graph.html
Normal file
File diff suppressed because one or more lines are too long
247
disable_publisher_warranty/graphify-out/graph.json
Normal file
247
disable_publisher_warranty/graphify-out/graph.json
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
{
|
||||||
|
"directed": false,
|
||||||
|
"multigraph": false,
|
||||||
|
"graph": {},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py",
|
||||||
|
"community": 2,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__manifest__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/__manifest__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_manifest_py",
|
||||||
|
"community": 6,
|
||||||
|
"norm_label": "__manifest__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py",
|
||||||
|
"community": 3,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__manifest__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/__manifest__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_manifest_py",
|
||||||
|
"community": 7,
|
||||||
|
"norm_label": "__manifest__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py",
|
||||||
|
"community": 4,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publisher_warranty.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "publisher_warranty.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "PublisherWarrantyContractDisabled",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L10",
|
||||||
|
"id": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "publisherwarrantycontractdisabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "_get_sys_logs()",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L14",
|
||||||
|
"id": "publisher_warranty_get_sys_logs",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "_get_sys_logs()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": ".update_notification()",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L29",
|
||||||
|
"id": "publisher_warranty_publisherwarrantycontractdisabled_update_notification",
|
||||||
|
"community": 1,
|
||||||
|
"norm_label": ".update_notification()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "DISABLED: Do not contact Odoo servers. Returns fake successful response.",
|
||||||
|
"file_type": "rationale",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L15",
|
||||||
|
"id": "publisher_warranty_rationale_15",
|
||||||
|
"community": 8,
|
||||||
|
"norm_label": "disabled: do not contact odoo servers. returns fake successful response."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "DISABLED: Do not send any data to Odoo servers. Just update local parame",
|
||||||
|
"file_type": "rationale",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L30",
|
||||||
|
"id": "publisher_warranty_rationale_30",
|
||||||
|
"community": 1,
|
||||||
|
"norm_label": "disabled: do not send any data to odoo servers. just update local parame"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "__init__.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/__init__.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py",
|
||||||
|
"community": 5,
|
||||||
|
"norm_label": "__init__.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publisher_warranty.py",
|
||||||
|
"file_type": "code",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L1",
|
||||||
|
"id": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"community": 0,
|
||||||
|
"norm_label": "publisher_warranty.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L10",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"_tgt": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"target": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L14",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"_tgt": "publisher_warranty_get_sys_logs",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"target": "publisher_warranty_get_sys_logs",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "method",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L29",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"_tgt": "publisher_warranty_publisherwarrantycontractdisabled_update_notification",
|
||||||
|
"source": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"target": "publisher_warranty_publisherwarrantycontractdisabled_update_notification",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L10",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"_tgt": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"source": "publisher_warranty_publisherwarrantycontractdisabled",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "contains",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L14",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"_tgt": "publisher_warranty_get_sys_logs",
|
||||||
|
"source": "publisher_warranty_get_sys_logs",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_publisher_warranty_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "rationale_for",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/publisher_warranty.py",
|
||||||
|
"source_location": "L30",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "publisher_warranty_rationale_30",
|
||||||
|
"_tgt": "publisher_warranty_publisherwarrantycontractdisabled_update_notification",
|
||||||
|
"source": "publisher_warranty_publisherwarrantycontractdisabled_update_notification",
|
||||||
|
"target": "publisher_warranty_rationale_30",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"relation": "imports_from",
|
||||||
|
"confidence": "EXTRACTED",
|
||||||
|
"source_file": "/Users/gurpreet/Github/Odoo-Modules/disable_publisher_warranty/models/__init__.py",
|
||||||
|
"source_location": "L2",
|
||||||
|
"weight": 1.0,
|
||||||
|
"_src": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py",
|
||||||
|
"_tgt": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py",
|
||||||
|
"source": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py",
|
||||||
|
"target": "users_gurpreet_github_odoo_modules_disable_publisher_warranty_models_init_py",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hyperedges": []
|
||||||
|
}
|
||||||
3
disable_publisher_warranty/models/__init__.py
Normal file
3
disable_publisher_warranty/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import publisher_warranty
|
||||||
|
|
||||||
47
disable_publisher_warranty/models/publisher_warranty.py
Normal file
47
disable_publisher_warranty/models/publisher_warranty.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Disable all publisher warranty / subscription checks for local development
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PublisherWarrantyContractDisabled(models.AbstractModel):
|
||||||
|
_inherit = "publisher_warranty.contract"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_sys_logs(self):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not contact Odoo servers.
|
||||||
|
Returns fake successful response.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty check DISABLED - not contacting Odoo servers")
|
||||||
|
return {
|
||||||
|
"messages": [],
|
||||||
|
"enterprise_info": {
|
||||||
|
"expiration_date": "2099-12-31 23:59:59",
|
||||||
|
"expiration_reason": "renewal",
|
||||||
|
"enterprise_code": self.env['ir.config_parameter'].sudo().get_param('database.enterprise_code', ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_notification(self, cron_mode=True):
|
||||||
|
"""
|
||||||
|
DISABLED: Do not send any data to Odoo servers.
|
||||||
|
Just update local parameters with permanent values.
|
||||||
|
"""
|
||||||
|
_logger.info("Publisher warranty update_notification DISABLED - no server contact")
|
||||||
|
|
||||||
|
# Set permanent valid subscription parameters
|
||||||
|
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||||
|
set_param('database.expiration_date', '2099-12-31 23:59:59')
|
||||||
|
set_param('database.expiration_reason', 'renewal')
|
||||||
|
|
||||||
|
# Clear any "already linked" parameters
|
||||||
|
set_param('database.already_linked_subscription_url', False)
|
||||||
|
set_param('database.already_linked_email', False)
|
||||||
|
set_param('database.already_linked_send_mail_url', False)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
# fusion_maintenance — Brainstorm & Handoff Brief
|
|
||||||
|
|
||||||
> Status: **research/brainstorm only — no code, no final decisions.** Written from a
|
|
||||||
> Claude Code *web* session that could **not** reach the private network (no Tailscale,
|
|
||||||
> no docker daemon, Supabase KB unreachable). Resume from a **Tailscale-connected env**
|
|
||||||
> (dev box or a host that can reach Westin production) and do the live inspection in
|
|
||||||
> Step 0 **before** committing to the design.
|
|
||||||
|
|
||||||
## Goal (user's words, paraphrased)
|
|
||||||
Automated maintenance follow-ups for mobility/accessibility equipment we've sold, to turn
|
|
||||||
service into **recurring revenue**. Reminder emails → client books maintenance → booking
|
|
||||||
happens in **real time** and **lands in our calendar**. Leverage Odoo Enterprise's
|
|
||||||
appointment system. Decide whether this lives in `fusion_repairs` or a new module — the
|
|
||||||
result must be **seamless and production-ready**.
|
|
||||||
|
|
||||||
## Decisions locked with the user (this session)
|
|
||||||
- **Same DB**: `fusion_claims` + `fusion_repairs` run on one database → new module may depend on both.
|
|
||||||
- **Enterprise `appointment` is available** → build real-time booking ON it (`appointment.type` /
|
|
||||||
`appointment.slot` / `calendar.event`), do **not** hand-roll a calendar.
|
|
||||||
- **Public self-serve booking** → reminder email carries a token link to a no-login slot picker
|
|
||||||
(extend the existing `/repairs/maintenance/book/<token>` pattern). Elderly clients shouldn't log in.
|
|
||||||
- **Target box for grounding = Westin production** (where `fusion_claims` runs day-to-day).
|
|
||||||
|
|
||||||
## Key findings from repo exploration
|
|
||||||
|
|
||||||
### `fusion_repairs` (v19.0.2.2.6) ALREADY has a maintenance engine — reuse it, don't fork
|
|
||||||
- `fusion.repair.maintenance.contract`: interval, due/last-service dates, state machine.
|
|
||||||
Auto-spawned on SO confirm when `product.template.x_fc_maintenance_interval_months > 0`.
|
|
||||||
- Daily reminder cron `cron_maintenance_due_reminders` → 30/7/1-day bands → branded email
|
|
||||||
`email_template_maintenance_due_reminder` with tokenized link `/repairs/maintenance/book/<token>`.
|
|
||||||
- Booking controller: `controllers/portal_maintenance_booking.py` — **single date-confirm form,
|
|
||||||
NO slot availability, NO conflict check, NO calendar event.** ← this is the real gap.
|
|
||||||
- Contract **roll-forward** on technician-task completion (`next_due_date += interval`).
|
|
||||||
- `fusion.repair.service.plan.subscription`: pre-paid visit plans (recurring-revenue primitive).
|
|
||||||
- Deps: `repair, maintenance, sale_management, stock, purchase, website, portal, fusion_tasks,
|
|
||||||
fusion_poynt, fusion_authorizer_portal`. ~8.3k LOC, 25+ models.
|
|
||||||
|
|
||||||
### `fusion_claims` (v19.0.9.2.0) is the ideal trigger source
|
|
||||||
- Claim container = `sale.order` (`x_fc_sale_type`: adp, odsp, wsib, insurance, march_of_dimes, …).
|
|
||||||
- **Equipment unit** = `sale.order.line.x_fc_serial_number` + `product_id`.
|
|
||||||
- **Equipment category** = `fusion.adp.device.code.device_type` (wheelchair, walker, hospital bed,
|
|
||||||
stair lift, porch lift, custom ramp, …) — matches the user's "sale groups".
|
|
||||||
- **Schedule anchors**: `x_fc_adp_delivery_date`, `x_fc_service_start_date`; gate on `x_fc_adp_approved`.
|
|
||||||
- Customer = `sale.order.partner_id`; prescriber = `x_fc_authorizer_id`.
|
|
||||||
- Already depends on `calendar, fusion_tasks, ai, fusion_ringcentral`.
|
|
||||||
|
|
||||||
## Proposed architecture (PENDING live verification)
|
|
||||||
**New module `fusion_maintenance`** depending on `fusion_repairs`, `fusion_claims`, `appointment`.
|
|
||||||
Reuses the existing contract/reminder/roll-forward engine; adds the 3 genuinely-missing pieces:
|
|
||||||
|
|
||||||
1. **`fusion.maintenance.policy`** (ops-configurable, no code per category):
|
|
||||||
`device_type` → `interval_months`, reminder bands, `service_product_id` (priced visit),
|
|
||||||
`appointment_type_id`, required technician skill. Turns "stair lift = 6 mo, $X" into data.
|
|
||||||
2. **Claims bridge** (daily cron): scan `fusion_claims` `sale.order.line` for delivered+approved
|
|
||||||
devices whose `device_type` matches an active policy → ensure a maintenance contract exists,
|
|
||||||
anchored at `delivery_date + interval`. Idempotent (key on serial / sale-line). Extend the
|
|
||||||
reused contract with `x_fc_source_claim_line_id`, `x_fc_device_type`, `x_fc_policy_id` so the
|
|
||||||
repairs path and claims path both feed **one** contract model.
|
|
||||||
3. **Real-time booking on `appointment`**: token link → slot picker backed by `appointment.type`
|
|
||||||
(partner pre-resolved from token, no login). Slot pick → real `calendar.event` → hook spawns
|
|
||||||
`repair.order` + technician task, assigns by skill/zone, advances reminder band, rolls contract
|
|
||||||
forward.
|
|
||||||
|
|
||||||
**Recurring revenue**: each policy carries `service_product_id` → booked visit drafts a priced
|
|
||||||
SO/invoice; optional pre-paid annual plan via existing `service.plan.subscription`; optional
|
|
||||||
door payment via existing `fusion_poynt`.
|
|
||||||
|
|
||||||
## STEP 0 — run on Westin production FIRST (grounding before any decision)
|
|
||||||
> Replace `APP`/`DB` with the real Westin container + database. CLAUDE.md rule #1: never code
|
|
||||||
> from memory — read the real Enterprise `appointment` source before building the booking layer.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# RESOLVED 2026-06-02 — Westin Odoo prod migrated OFF Digital Ocean onto the on-prem Proxmox
|
|
||||||
# cluster. Old DO IPs (152.42.146.204 / 178.128.229.92) are DEAD (:22 timeout). Live box:
|
|
||||||
# host `odoo-westin` = 192.168.1.40 via the `supabase-prod` Tailscale jump (Windows OpenSSH
|
|
||||||
# ProxyCommand → run `ssh odoo-westin ...` from PowerShell). App container `odoo-dev-app`
|
|
||||||
# (odoo:19, Enterprise); DB container `odoo-dev-db`; DB `westin-v19`; user `odoo` (local-socket
|
|
||||||
# trust inside odoo-dev-db). Enterprise addons → /mnt/enterprise-addons, custom → /mnt/extra-addons.
|
|
||||||
# SQL: ssh odoo-westin 'docker exec odoo-dev-db psql -U odoo -d westin-v19 -c "..."'
|
|
||||||
# FS read: ssh odoo-westin 'docker exec odoo-dev-app sed -n 1,160p /mnt/enterprise-addons/...'
|
|
||||||
APP=odoo-dev-app ; DB=westin-v19 ; DBC=odoo-dev-db
|
|
||||||
|
|
||||||
# 1) Install matrix — confirm same-DB + Enterprise appointment present + versions
|
|
||||||
docker exec "$APP" psql -U odoo -d "$DB" -c \
|
|
||||||
"SELECT name,state,latest_version FROM ir_module_module \
|
|
||||||
WHERE name IN ('fusion_claims','fusion_repairs','fusion_maintenance','calendar','maintenance','repair') \
|
|
||||||
OR name LIKE 'appointment%' ORDER BY name;"
|
|
||||||
|
|
||||||
# 2) Real device_type distribution (drives per-category policies)
|
|
||||||
docker exec "$APP" psql -U odoo -d "$DB" -c \
|
|
||||||
"SELECT device_type, count(*) FROM fusion_adp_device_code GROUP BY device_type ORDER BY 2 DESC;"
|
|
||||||
|
|
||||||
# 3) Locate the Enterprise appointment source (read, don't guess the API)
|
|
||||||
docker exec "$APP" bash -lc 'ls -d /mnt/enterprise-addons/appointment 2>/dev/null || \
|
|
||||||
find / -maxdepth 6 -type d -name appointment 2>/dev/null | grep -i addons | head'
|
|
||||||
|
|
||||||
# 4) Appointment model surface to build booking on (adjust path from #3)
|
|
||||||
docker exec "$APP" cat <appointment_path>/models/appointment_type.py | head -160
|
|
||||||
docker exec "$APP" ls <appointment_path>/controllers/ # find the public booking controller
|
|
||||||
|
|
||||||
# 5) How fusion_repairs maintenance contracts already look in live data
|
|
||||||
docker exec "$APP" psql -U odoo -d "$DB" -c \
|
|
||||||
"SELECT state, count(*) FROM fusion_repair_maintenance_contract GROUP BY state;"
|
|
||||||
```
|
|
||||||
|
|
||||||
## STEP 0 — RESULTS (ran 2026-06-02 against Westin prod `westin-v19`)
|
|
||||||
> Grounding facts only — **no design decisions made**. These correct several assumptions above.
|
|
||||||
|
|
||||||
**Connection (resolved):** host `odoo-westin` (192.168.1.40) via the `supabase-prod` Tailscale jump.
|
|
||||||
App container `odoo-dev-app` (odoo:19, Enterprise), DB container `odoo-dev-db`, DB `westin-v19`,
|
|
||||||
user `odoo`. Old Digital Ocean boxes are DEAD — Westin migrated on-prem.
|
|
||||||
|
|
||||||
**1) Install matrix** — `appointment` **19.0.1.3 installed** (+ `appointment_account_payment`,
|
|
||||||
`_crm`, `_hr`, `_microsoft_calendar`, `_sms`). All deps present: `calendar`, `maintenance`, `repair`,
|
|
||||||
`sale_management`, `portal`, `website`, `resource`, `phone_validation`, `web_gantt`. `fusion_claims`
|
|
||||||
**19.0.9.2.0 installed**. `fusion_repairs` and `fusion_maintenance` are **absent entirely** (no
|
|
||||||
records). → a module depending on `appointment` installs cleanly; "reuse the fusion_repairs engine"
|
|
||||||
means *deploy fusion_repairs to Westin first* (heavy) **or** own a lean contract model here. Note
|
|
||||||
Odoo's native `maintenance` (CMMS) is installed — an under-considered third reuse option.
|
|
||||||
|
|
||||||
**2) device_type** — 119 distinct values, but `fusion.adp.device.code` is the ADP billing-code
|
|
||||||
**CATALOG** (`_order='device_type, device_code'`), so counts are catalog codes per type, **NOT units
|
|
||||||
installed**. Top entries are seating COMPONENTS (Seat Cushion 564, Back Support 375, Headrest 193).
|
|
||||||
The maintainable **equipment classes** ≈ wheelchairs (manual + power tilt), power bases, power
|
|
||||||
scooters, wheeled walkers / walking frames, paediatric standing frames, specialty strollers (~6-8
|
|
||||||
clean categories). → `device_type` can't be a 1:1 policy key (119 values, mostly parts); needs a
|
|
||||||
grouping/whitelist. **Real install base sized on `sale.order.line`** (`x_fc_adp_device_type` [stored compute from
|
|
||||||
product's `x_fc_adp_device_code_id.device_type`], `x_fc_serial_number`, `x_fc_adp_approved`; delivery
|
|
||||||
dates `x_fc_adp_delivery_date` / `x_fc_service_start_date`) — **see the Install-base sizing block below.**
|
|
||||||
|
|
||||||
**3) + 4) Enterprise appointment source** — `/mnt/enterprise-addons/appointment`. The no-login token
|
|
||||||
slot-picker is **mostly NATIVE — don't hand-roll it**: public booking (`auth="public"`), invite
|
|
||||||
tokens (`appointment.invite`, `/appointment/<id>?…invite_token`), live availability
|
|
||||||
(`/appointment/<id>/update_available_slots`, jsonrpc/public), slot submit → real `calendar.event`
|
|
||||||
(`/appointment/<id>/submit`), auto/manual staff+resource assignment, capacity, booked/cancelled mail
|
|
||||||
templates. Model `appointment.type`; controller `controllers/appointment.py`. → the module mainly
|
|
||||||
needs to: seed an `appointment.type` per category, drop a partner-bound invite link into the reminder
|
|
||||||
email, and hook `calendar.event` create → spawn the service task + advance the contract.
|
|
||||||
`appointment_account_payment` is installed → native pay-to-book is on the table for the revenue mechanic.
|
|
||||||
|
|
||||||
**5) Maintenance-contract state** — `relation "fusion_repair_maintenance_contract" does not exist`
|
|
||||||
→ confirms the fusion_repairs maintenance engine is **not** on Westin.
|
|
||||||
|
|
||||||
**Headline correction:** Westin's ADP data has **zero** stair lifts / porch lifts / ramps / hospital
|
|
||||||
beds — those belong to the fusion_repairs / EN-Tech (mobility) domain. Westin's recurring-revenue
|
|
||||||
play is **wheelchairs / power bases / scooters / walkers / seating**. Open questions updated below.
|
|
||||||
|
|
||||||
**Install-base sizing (ran 2026-06-02 — the REAL units, complementing #2's catalog counts).** Big tell:
|
|
||||||
serial numbers are captured **~only on actual equipment** (every part/option/mod device_type shows 0
|
|
||||||
serials), so `x_fc_serial_number` is already a de-facto "trackable unit" marker — convenient, because the
|
|
||||||
bridge's idempotency key is the serial.
|
|
||||||
|
|
||||||
- **Addressable base ≈ 138 serial-tracked units across ~136 customers** (all funders). By equipment
|
|
||||||
family (serial-tracked / of which delivered): **Walkers & walking frames 68 (55)**, **Wheelchairs 45
|
|
||||||
(40)**, **Power bases 7 (6)**, **Scooters 4 (3)**, plus **14 units with no ADP device_type** (likely
|
|
||||||
private-pay) and 1 misc.
|
|
||||||
- **Funder split** (serial-tracked): adp 109, direct_private 13, adp_odsp 10, march_of_dimes 7;
|
|
||||||
wsib / insurance / standalone-odsp / rental / regular = **0 serials**. → an ADP-only gate
|
|
||||||
(`x_fc_adp_approved`) captures ~110 and **misses ~28** real units. The bridge should likely key on
|
|
||||||
**serial (funder-agnostic)**, not approval.
|
|
||||||
- **Two data gaps the design must absorb:** (a) the 14 serial units with no ADP device_type can't be
|
|
||||||
classified by a device_type→policy map → need a product-level or manual category override; (b) non-ADP
|
|
||||||
units have no `x_fc_adp_delivery_date` → the contract anchor (`delivery_date + interval`) needs a
|
|
||||||
fallback (invoice/order date).
|
|
||||||
- Deliveries span **2022-10 → 2026-05** (active program) — history to anchor intervals + a live pipeline.
|
|
||||||
- Top serial-tracked device_types: Adult Wheeled Walker Type 3 (47), Adult Manual Dynamic Tilt Type 5
|
|
||||||
Wheelchair (23), Adult Lightweight Performance Type 3 (11), Adult Lightweight Standard Type 1 (10),
|
|
||||||
Adult Wheeled Walker Type 2 (9), Adult Power Base Type 3 (5), Power Scooter (3). (1 line ≈ 1 unit;
|
|
||||||
equipment device_types are 1 base line each.)
|
|
||||||
|
|
||||||
## Open questions to resolve with the user (in the connected session)
|
|
||||||
- **MVP cut**: which categories first? Sizing surfaces a real tension: **by volume** it's walkers (68) +
|
|
||||||
wheelchairs (45) ≈ 82% of the base, but rollators/walkers are mechanically low-service; **by
|
|
||||||
service-revenue-per-unit** the targets are the powered units (power bases 7 + scooters 4 + power
|
|
||||||
wheelchairs) — high maintenance value but only ~11–15 units today. Volume vs. margin — or phase it
|
|
||||||
(powered units first to prove the booking loop, then walkers/manual chairs for reach)?
|
|
||||||
- **Revenue mechanic**: auto-draft a priced SO/invoice per booking, vs. pre-paid annual plan, vs.
|
|
||||||
pay-at-door via Poynt — which is the default?
|
|
||||||
- **Technician assignment**: auto-assign by skill+zone at booking time, or leave dispatch manual
|
|
||||||
(fusion_tasks) and only reserve the calendar slot?
|
|
||||||
- **Booking-portal strategy**: Step 0 shows Enterprise `appointment` already ships public,
|
|
||||||
token-based real-time booking (`appointment.invite` + `/appointment/<id>/...`, `auth="public"`).
|
|
||||||
Ride on that (generate an invite per reminder, partner pre-bound, no login) vs. a custom
|
|
||||||
`/maintenance/book/<token>` route? (The `/repairs/...` route is moot — fusion_repairs isn't on Westin.)
|
|
||||||
|
|
||||||
## Applicable CLAUDE.md rules (don't relearn the hard way)
|
|
||||||
- Rule #1: read reference files from the running instance before coding (esp. the appointment source).
|
|
||||||
- Odoo 19: `res.users.group_ids` (not `groups_id`); `ir.cron` has no `numbercall`; declarative
|
|
||||||
`models.Constraint`/`models.Index`; HTTP routes `type="jsonrpc"`; OWL uses standalone `rpc()`.
|
|
||||||
- No `sale.subscription` model exists — a subscription is a `sale.order` with `is_subscription=True`.
|
|
||||||
- New fields use `x_fc_` prefix; Canadian English; `$` Monetary + `currency_id`.
|
|
||||||
- Route attachment opens through `fusion_pdf_preview` (`att.action_fusion_preview(...)`).
|
|
||||||
- Tests need `--http-port=0 --gevent-port=0`. Westin prod is Enterprise; local dev is Community
|
|
||||||
(so the appointment-dependent module can't be installed/tested on `odoo-modsdev-app`).
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# fusion_centralize_billing — Session Handoff (2026-05-27)
|
|
||||||
|
|
||||||
Resume point for the centralized-billing initiative. Read this first, then continue
|
|
||||||
from **"Decision pending"** below.
|
|
||||||
|
|
||||||
## Where we are
|
|
||||||
|
|
||||||
- **Sub-project #1 (core billing engine): DONE and on `main`** (tip `d770c0c3`, pushed to
|
|
||||||
GitHub + Gitea).
|
|
||||||
- 11/11 plan tasks, TDD, Opus code-reviewed; all Critical/High bugs fixed
|
|
||||||
(cross-billing cron → match by `plan_id`; `/usage` authz vs IDOR; input validation →
|
|
||||||
4xx not 500; correct billing-period window; idempotency scoped to `(sub, metric, key)`;
|
|
||||||
webhook sign-exact-bytes + event-id + SSRF guard).
|
|
||||||
- **39 tests green on Odoo 19 Enterprise.**
|
|
||||||
- Note: the 14 billing commits were rebased off the old login-audit/helpdesk stack and
|
|
||||||
landed cleanly on `main`.
|
|
||||||
|
|
||||||
- **`fusion_login_audit`: also landed on `main`** (2026-05-27). Its 19 commits were rebased
|
|
||||||
onto `main` and the `feat/fusion-login-audit` branch was deleted. This also restored
|
|
||||||
Odoo-19 rules #9–14 in `CLAUDE.md`, which had gone missing on `main` when billing landed
|
|
||||||
alone (they were authored alongside login_audit and never existed on the old base).
|
|
||||||
- A concurrent `feat/helpdesk-customer-followup` session still carries pre-landing copies
|
|
||||||
of the billing + login_audit commits; when it merges, replay its helpdesk-only commits
|
|
||||||
onto `main`.
|
|
||||||
|
|
||||||
- **Reference docs (on `main`):**
|
|
||||||
- Spec: `docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.md`
|
|
||||||
- Core plan: `docs/superpowers/plans/2026-05-27-fusion-centralize-billing-core.md`
|
|
||||||
|
|
||||||
## Next: sub-project #2 — NexaCloud adapter + dual-run reconciliation
|
|
||||||
|
|
||||||
Per spec §12, each sub-project is its own spec → plan → build cycle. #2 decomposes into
|
|
||||||
four chunks (dependency order):
|
|
||||||
|
|
||||||
| Chunk | What | Risk |
|
|
||||||
|-------|------|------|
|
|
||||||
| **2a — Mapping + importer** | Read `nexacloud` DB → create `res.partner` + `account.link`, `product.template` + subscription plans, one subscription `sale.order` per deployment | **Low** — read-only on NexaCloud, writes only into Odoo |
|
|
||||||
| **2b — Usage metering wiring** | NexaCloud `usage_metering.py` pushes CPU-seconds → Odoo `/usage`; verify aggregation → draft invoice w/ quota + overage + HST | Edits NexaCloud code |
|
|
||||||
| **2c — Control loop** | NexaCloud consumes Odoo's outbound webhooks (`invoice.payment_failed` → suspend via existing `network_isolation`/`throttle_checker`; `subscription.terminated` → deprovision) | Edits NexaCloud code |
|
|
||||||
| **2d — Dual-run reconciliation** | `fusion.billing.reconciliation` diffs Odoo-computed vs NexaCloud-actual per customer/period for ≥ 1 cycle before any flip | Safety gate before flipping real billing |
|
|
||||||
|
|
||||||
The core engine already built the *receiving* side (`/usage`, webhook engine, charge math).
|
|
||||||
#2 is about **connecting NexaCloud to it and proving the numbers match before flipping.**
|
|
||||||
|
|
||||||
## Decision pending (resume here)
|
|
||||||
|
|
||||||
We were in the `superpowers:brainstorming` flow for #2 and stopped at: **which slice to
|
|
||||||
start with?**
|
|
||||||
|
|
||||||
- **(recommended) 2a — Mapping + importer** — lowest risk, foundation for everything else.
|
|
||||||
- 2d — Reconciliation first (front-load the trust mechanism).
|
|
||||||
- Full #2 design as one spec, then one plan.
|
|
||||||
- Just write the #2 plan, no code this session.
|
|
||||||
|
|
||||||
## Open questions to resolve before building #2
|
|
||||||
|
|
||||||
- **Spec §15 Q2 — NexaCloud billing granularity:** confirm **one subscription per
|
|
||||||
deployment** (spec leans this way) vs one subscription per customer with deployment line
|
|
||||||
items.
|
|
||||||
- **Access / environments needed:**
|
|
||||||
- Read access to the `nexacloud` DB schema (LXC 102 / its Postgres on LXC 201) to design
|
|
||||||
the importer mapping.
|
|
||||||
- A NexaCloud staging or safe path for 2b/2c (they edit live NexaCloud code).
|
|
||||||
- Test target for the Odoo side stays the odoo-trial Enterprise sandbox.
|
|
||||||
- **Resolved already:** Stripe is one account (`acct_1ShlA9IkwUB1dVox`) for everything — no
|
|
||||||
account migration (spec §11 / §15 Q1). Branch strategy — land on `main`, branch new work
|
|
||||||
off `main`.
|
|
||||||
|
|
||||||
## How to run / test
|
|
||||||
|
|
||||||
- **Billing tests:** `bash scripts/fcb_test_on_trial.sh` from repo root → pass = `FCB_EXIT=0`
|
|
||||||
(~1–2 min). Syncs the module to the odoo-trial Enterprise sandbox (Proxmox VM 316, db
|
|
||||||
`trial`) and runs `--test-enable`. Local dev Odoo is Community and **cannot** install this
|
|
||||||
module.
|
|
||||||
|
|
||||||
## Branch hygiene (lesson from this session)
|
|
||||||
|
|
||||||
Cut each new feature branch from `main`, and land it before starting the next. For any
|
|
||||||
cross-branch git surgery, use a **throwaway `git worktree`** — never switch the shared
|
|
||||||
working dir's branch, because a concurrent session may be working on it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UPDATE — sub-project #2 complete (2026-05-27, later session)
|
|
||||||
|
|
||||||
All four chunks of #2 are now built. The brainstorm "which slice" question resolved to
|
|
||||||
2a-first; everything else followed.
|
|
||||||
|
|
||||||
**Done + on `main` in `Odoo-Modules` (fully tested on odoo-trial, suite `FCB_EXIT=0`):**
|
|
||||||
- **2a — importer** (`fusion.billing.import.wizard`): read-only `psycopg2` reader split
|
|
||||||
from pure-Odoo writes; users→partners+links, plans→`cpu_seconds` charge catalog
|
|
||||||
(`plan_id` NULL), deployments→one **draft shadow** `sale.order` each with the flat price.
|
|
||||||
Shadow-safe by construction (draft + no token + NULL `plan_id`). Idempotent, dry-run,
|
|
||||||
Test-Connection guard, README runbook.
|
|
||||||
- **2d — reconciliation** (`fusion.billing.reconciliation`): `_compute_reconciliation` +
|
|
||||||
`_reconcile_rows` (Odoo flat+overage vs NexaCloud actual, status match/delta), reader for
|
|
||||||
NexaCloud usage+invoice actuals, "Run Reconciliation" button. **Upsert key is
|
|
||||||
`(service, external_subscription_id, period)`** — per subscription, so a customer with
|
|
||||||
two deployments doesn't collide.
|
|
||||||
- **/usage enabler**: `_api_record_usage` resolves a subscription by the source app's own
|
|
||||||
id (`x_fc_nexacloud_subscription_id`) so NexaCloud can push against shadow subs.
|
|
||||||
- Core-engine bug fixed in passing: `charge.price_per_unit` is now `Float(16,6)` and
|
|
||||||
`_compute_billable` keeps 6-dp precision (was `Monetary`/cent-rounded → would under-bill
|
|
||||||
sub-cent rates and drift from NexaCloud's 4-dp amounts).
|
|
||||||
|
|
||||||
**Code-complete in `Nexa-Cloud` (feature-flagged, NOT deployed, NOT integration-tested):**
|
|
||||||
- **2b — usage push**: `services/odoo_billing_client.py` + a hook in `usage_metering.py`
|
|
||||||
posting cpu-seconds to Odoo `/usage`. **2c — control loop**:
|
|
||||||
`routers/odoo_billing.py` (`POST /api/v1/billing/webhooks/central`, HMAC-verified) +
|
|
||||||
`services/odoo_billing_integration.py` (suspend/restore/deprovision). All INERT unless
|
|
||||||
`ODOO_BILLING_ENABLED`. Implemented as NEW modules + edits to clean files only —
|
|
||||||
NexaCloud `main` had concurrent **Cursor uncommitted WIP** (`routers/billing.py`,
|
|
||||||
`scheduler.py`, `stripe_service.py`, `models/billing.py`, …) which was deliberately not
|
|
||||||
touched. Commits: `94542ec` + `956abb2` (only my files staged).
|
|
||||||
|
|
||||||
**Deployment status (2026-05-27):**
|
|
||||||
- **odoo-nexa (production `nexamain`): DEPLOYED** — `fusion_centralize_billing` (core + 2a
|
|
||||||
+ 2d) **fresh-installed** (#1 had never actually been deployed here; `DIR_ABSENT` before).
|
|
||||||
`ir_module_module.state = installed`, `odoo-nexa-app` healthy. **INERT**: no
|
|
||||||
`nexacloud_dsn`, all charges `plan_id` NULL (rating cron no-op), no webhooks queued
|
|
||||||
(dispatch cron no-op), inbound API 401s with no key configured. Synced to
|
|
||||||
`/opt/odoo/custom-addons` + `-i` via the restart-safe recipe.
|
|
||||||
- **NexaCloud (prod, `vps.nexasystems.ca` / 192.168.1.250): DEPLOYED — INERT.** Did NOT
|
|
||||||
use `./deploy.sh` (it `rsync --delete`s the working tree → would have shipped the
|
|
||||||
concurrent **uncommitted Cursor WIP** (7 files) AND wiped the gitignored prod `.env`
|
|
||||||
files). Instead deployed **surgically**: rsync of ONLY my 6 committed billing files (no
|
|
||||||
`--delete`; `.env` + Cursor's files untouched), `docker compose build backend`,
|
|
||||||
**boot-tested in a throwaway container** (`run --rm --no-deps backend python -c "import
|
|
||||||
app.main"` → BOOT_OK) before swapping, then `up -d backend`. `nexacloud-api` healthy,
|
|
||||||
`/health` OK. Feature OFF: `ODOO_BILLING_ENABLED` unset → `/billing/webhooks/central`
|
|
||||||
returns 404 and no usage is pushed. Activate later by setting `ODOO_BILLING_*` in
|
|
||||||
`/opt/nexacloud/.env` (+ compose env passthrough) once the Odoo side is wired.
|
|
||||||
**NOTE:** Cursor's 7-file WIP remains uncommitted locally and was never deployed — when
|
|
||||||
Cursor finishes, a normal `./deploy.sh` will ship it (and re-sync `.env`).
|
|
||||||
|
|
||||||
**Dual-run stand-up results (2026-05-27) — STOPPED here for review, NOT flipped:**
|
|
||||||
- Read-only role `odoo_billing_ro` created on nexacloud Postgres (192.168.1.50); DSN set in
|
|
||||||
`ir.config_parameter` `fusion_billing.nexacloud_dsn` on nexamain. Test Connection OK
|
|
||||||
(read 7 users / 232 plans / 87 subscriptions).
|
|
||||||
- **Shadow import committed on nexamain**: 7 partners, 232 plan catalogs, 87 draft shadow
|
|
||||||
subscriptions; 0 skipped, 0 failed. (NOTE: importer takes ALL plans/subs regardless of
|
|
||||||
active status → ~464 NC-* products now in the prod ERP catalog. Consider filtering to
|
|
||||||
`is_active` plans / active subscriptions, or prune the shadow records — all reversible.)
|
|
||||||
- **Reconciliation pass**: 9 (sub,period) rows had real billing activity → **2 match, 7
|
|
||||||
delta**, 0 failed. The 7 deltas, MUST resolve before flipping:
|
|
||||||
1. **One-off / non-subscription invoices** (3 rows: $877.99, $872.66, $32.20) — nexacloud
|
|
||||||
invoices with NULL subscription_id (fees/manual/credits); not modeled per-subscription.
|
|
||||||
2. **List-price ≠ actual-invoiced** (4 rows: Odoo $200/$50 vs actual ~$9.1x) — likely
|
|
||||||
proration or NexaCloud invoicing ≠ plan list price.
|
|
||||||
- **2d bug surfaced (analysis-only, not safety):** `_reconcile_rows` with an empty
|
|
||||||
`subscription_external_id` matches NULL-field orders instead of skipping → spurious
|
|
||||||
delta rows for the one-off invoices. Add `if not sub_ext: skip`.
|
|
||||||
|
|
||||||
**Remaining before go-live (gated on infra / ops you do):**
|
|
||||||
1. Grant the read-only DSN (`fusion_billing.nexacloud_dsn`) — see the module README — then
|
|
||||||
Test Connection → dry-run import → review → real import.
|
|
||||||
2. Run a dual-run cycle (Run Reconciliation), confirm all rows `match`.
|
|
||||||
3. **2c needs the Odoo side to actually EMIT** `invoice.payment_failed` /
|
|
||||||
`payment_succeeded` / `subscription.terminated` webhooks with `deployment_id` in the
|
|
||||||
payload — that emission isn't wired yet (it belongs to the live billing flow). The
|
|
||||||
NexaCloud receiver is built to that contract; confirm the payload shape when wiring it.
|
|
||||||
4. Integration-test + deploy the NexaCloud changes (no test harness in that repo).
|
|
||||||
5. The flip: set `charge.plan_id`, attach Stripe tokens, confirm the shadow subs.
|
|
||||||
|
|
||||||
Specs/plans: `specs/2026-05-27-nexacloud-billing-importer-design.md`,
|
|
||||||
`specs/2026-05-27-nexacloud-reconciliation-design.md`, and the matching plans.
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# KICKOFF BRIEF — Implement "Technician Service Booking & Auto-Quote" (hands-off)
|
|
||||||
|
|
||||||
You are a fresh Claude Code session. **Implement this feature end-to-end, autonomously, from the
|
|
||||||
plans below.** The design is already locked through brainstorming — **do NOT re-design or
|
|
||||||
re-brainstorm.** Build it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Mission
|
|
||||||
|
|
||||||
Replace the raw `fusion.technician.task` booking modal with a polished **OWL "Book a Service"
|
|
||||||
wizard** that: captures the client (incl. brand-new clients inline), books the technician task,
|
|
||||||
prices the call-out from an **editable rate table**, and **auto-creates a draft repair Sale Order**
|
|
||||||
— with correct, consistent timezone handling. Works in dark + light.
|
|
||||||
|
|
||||||
## 2. Read these first, in order
|
|
||||||
|
|
||||||
1. `K:\Github\Odoo-Modules\CLAUDE.md` (repo Odoo-19 rules) + the global `K:\Github\CLAUDE.md`.
|
|
||||||
2. Spec: `docs/superpowers/specs/2026-06-03-technician-service-booking-design.md`
|
|
||||||
3. **Plan 1** (do first): `docs/superpowers/plans/2026-06-03-service-rates-foundation-plan.md`
|
|
||||||
4. **Plan 2** (do second): `docs/superpowers/plans/2026-06-03-service-booking-wizard-plan.md`
|
|
||||||
5. UI source of truth (port its markup/CSS): `docs/superpowers/mockups/technician-booking-wizard.html`
|
|
||||||
|
|
||||||
The plans are bite-sized (TDD, exact files, full code). They are the authority — follow them
|
|
||||||
task-by-task. The spec/mockup are context.
|
|
||||||
|
|
||||||
## 3. Method
|
|
||||||
|
|
||||||
- Use the **`superpowers:subagent-driven-development`** skill (the plan headers require it). One
|
|
||||||
task at a time; write test → implement → verify → **commit per task** with the messages in the plan.
|
|
||||||
- **Order: Plan 1 fully, then Plan 2** (Plan 2 consumes Plan 1's `fusion.service.rate`).
|
|
||||||
- Before writing any model/view/OWL code, obey repo rule #1: **read the real reference from Docker
|
|
||||||
first** (`docker exec odoo-modsdev-app cat …` or, for the Enterprise classes, read the on-disk
|
|
||||||
source) — never code Odoo APIs from memory. The plans flag the specific signatures to confirm
|
|
||||||
(`_get_local_tz`, `_compute_datetimes`, `_calculate_travel_time`, real task field names like
|
|
||||||
`in_store`/`client_name`/`address_lat`, the `crm.tag` vs `sale.order` tag model).
|
|
||||||
|
|
||||||
## 4. Branch
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git -C K:\Github\Odoo-Modules checkout main
|
|
||||||
git -C K:\Github\Odoo-Modules checkout -b claude/technician-service-booking
|
|
||||||
```
|
|
||||||
Create it **off `main`** — NOT off `claude/fusion-schedule-audit-fixes` (that branch has unrelated
|
|
||||||
calendar-sync fixes). The spec/plans/mockup are already on disk under `docs/superpowers/`; keep them.
|
|
||||||
|
|
||||||
## 5. Hard constraints (do not violate)
|
|
||||||
|
|
||||||
- **Odoo 19 idioms** (from CLAUDE.md): declarative `models.Constraint` / `models.Index` (never
|
|
||||||
`_sql_constraints`); `group_ids` not `groups_id`; HTTP routes `type="jsonrpc"`; backend OWL uses
|
|
||||||
**standalone `rpc()`** from `@web/core/network/rpc` (not `useService("rpc")`), client action
|
|
||||||
`static props = ["*"]`; **dark mode** = branch on `$o-webclient-color-scheme` at SCSS compile
|
|
||||||
time and register the SCSS in **both** `web.assets_backend` **and** `web.assets_web_dark`; new
|
|
||||||
fields use the **`x_fc_`** prefix; **Canadian English**; any `message_post(body=…)` HTML wrapped
|
|
||||||
in `Markup()`.
|
|
||||||
- **Enterprise-only:** `fusion_claims` pulls `ai` → it **cannot install on local Community
|
|
||||||
(`odoo-modsdev`)**. Do **not** attempt `-d modsdev -u fusion_claims`. (`fusion_tasks` alone may
|
|
||||||
install locally — the tz-fix test in Plan 2 Task 1 can be tried there; everything else is clone-only.)
|
|
||||||
- **The design is LOCKED** — implement exactly §6 below; don't add scope or re-open decisions.
|
|
||||||
|
|
||||||
## 6. Locked design (build exactly this)
|
|
||||||
|
|
||||||
- **Time:** 12-hour **AM/PM** entry on the wizard (custom control — Odoo's native widget is 24h).
|
|
||||||
Fix the `fusion_tasks` tz bug: the `_inverse_datetime_*` methods must use `self._get_local_tz()`
|
|
||||||
(same resolver as `_compute_datetimes`), not `self.env.user.tz`.
|
|
||||||
- **Client:** inline **new-client** (name / phone / email / address) on the page; **no forced SO**
|
|
||||||
(relax `fusion_claims` `_check_order_link` to a no-op); find-or-create the `res.partner` on save
|
|
||||||
(match by email then phone).
|
|
||||||
- **View:** a **full OWL client action** wizard (complete design freedom), ported from the mockup,
|
|
||||||
dark + light.
|
|
||||||
- **Pricing → SO:** pick service type → call-out fee → **auto draft repair `sale.order`** with the
|
|
||||||
call-out line **+ auto per-km line** for Rush/After-Hours (qty = `travel_distance_km × 2`,
|
|
||||||
$0.70/km). On-screen **estimate is UI-only** (labour/parts added later as actuals). Tag the SO
|
|
||||||
(`x_fc_is_service_repair` + a "Service Repair" tag).
|
|
||||||
- **Rates are an editable table** — `fusion.service.rate` with a **Service Rates** menu. The card
|
|
||||||
only **seeds** it (`noupdate=1`). Pricing is read from this table, never hardcoded.
|
|
||||||
- **Rate card seed:** Standard call $95 / Rush $120 / After-Hours $140; Lift & Elevating $160 /
|
|
||||||
**Rush $185** / **After-Hours $205** (the $185/$205 are *suggested* fills — seed them but they're
|
|
||||||
confirm-pending; leave a code comment). Labour: on-site $85, in-shop $75 (reuse existing `LABOR`
|
|
||||||
product), lift $110. Per-km $0.70 ×2-way. Delivery/setup: local $35 / outside $60 / rush $60+km /
|
|
||||||
lift-chair $120 / bed $120 / stairlift $300 / removal $300. **In-shop = no call-out, labour @ $75.**
|
|
||||||
- **Module split:** the tz fix goes in **`fusion_tasks`**; everything else (rate model, products,
|
|
||||||
menu, resolver, SO builder, `action_book_from_wizard`, controller, OWL wizard, SCSS, entry point)
|
|
||||||
goes in **`fusion_claims`**.
|
|
||||||
|
|
||||||
## 7. Verification (you probably can't reach the Enterprise clone — handle both cases)
|
|
||||||
|
|
||||||
- **Always do (no Odoo needed):** after each Python file, run `python -m py_compile <file>` and
|
|
||||||
`python -m pyflakes <file>` (or `docker exec odoo-modsdev-app python3 -m pyflakes …`). **Fix every
|
|
||||||
warning you introduce.** This is your local gate.
|
|
||||||
- **Full tests + smoke require a Westin Enterprise clone.** A one-command harness already exists:
|
|
||||||
`scripts/verify_service_booking.sh` (runs on the `odoo-westin` host: clones the DB, the
|
|
||||||
orphaned-tax-FK cleanup, stages the branch, `-u` + tests, PASS/FAIL; `--deploy` ships on green).
|
|
||||||
- If you have access to `odoo-westin`: push the branch, then run that script (verify-only first).
|
|
||||||
- If you do **not**: finish all code, ensure `py_compile`/`pyflakes` are clean, **commit the
|
|
||||||
branch task-by-task**, and clearly report **"clone-verification pending — run
|
|
||||||
`scripts/verify_service_booking.sh` on odoo-westin."** Do not fake a green test.
|
|
||||||
- **Never deploy to prod yourself.** Leave `--deploy` to the human.
|
|
||||||
|
|
||||||
## 8. Definition of done
|
|
||||||
|
|
||||||
- [ ] Branch `claude/technician-service-booking` off `main`.
|
|
||||||
- [ ] Plan 1 + Plan 2 implemented, **committed task-by-task** with the plans' commit messages.
|
|
||||||
- [ ] `py_compile` + `pyflakes` clean on every touched `.py`.
|
|
||||||
- [ ] OWL wizard renders the mockup layout in **both** light and dark bundles.
|
|
||||||
- [ ] Either **clone-verified GREEN** via the script, **or** branch committed + verification
|
|
||||||
explicitly flagged pending (with the exact command to run).
|
|
||||||
- [ ] A short final report: what was built, files changed, how to verify + deploy (`scripts/verify_service_booking.sh`),
|
|
||||||
and the one open business item (confirm Lift Rush/After-Hours $185/$205).
|
|
||||||
|
|
||||||
## 9. Don't
|
|
||||||
|
|
||||||
- Don't test on `odoo-modsdev` (Community — `fusion_claims` won't install).
|
|
||||||
- Don't re-brainstorm or change the design in §6.
|
|
||||||
- Don't hardcode prices (they live in `fusion.service.rate`).
|
|
||||||
- Don't deploy to prod or run `--deploy` — hand that to the human.
|
|
||||||
- Don't change the suggested $185/$205 silently — keep them, flag them confirm-pending.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Optional: launch it headless
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# from the repo root, on a machine with this checkout:
|
|
||||||
claude -p "$(cat docs/superpowers/EXECUTE-technician-service-booking.md)" --permission-mode acceptEdits
|
|
||||||
```
|
|
||||||
…or just paste this file into a fresh Claude Code session and say "go".
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user