# Wiring the dashboards to a real Odoo instance

This document explains how to swap the mock `ODOO_DATA` block in each dashboard for real
API responses from `wakecap.odoo.com` (or any Odoo 18.0 Enterprise instance).

The dashboards are intentionally structured so that **only the `ODOO_DATA` object at the top
of the `<script>` needs to change** — all rendering logic stays the same. Every field name
used in the mock matches the live Odoo schema verbatim.

---

## 1. Where the mock lives

Open either dashboard HTML. Near the middle of the file (after the CSS + HTML, before the
app logic) you'll find:

```html
<!-- ============================================================
     MOCK DATA — replace with Odoo API responses.
     ============================================================ -->
<script>
const ODOO_DATA = {
  warehouses: [ ... ],
  stores: [ ... ],
  product_categories: [ ... ],
  items: [ ... ],
  stock_picking: [ ... ],
  receipt_lines: [ ... ],
  license: { ... }
};
</script>
```

Replace each field's array/object with the result of the corresponding Odoo call below.

---

## 2. Authentication

Odoo exposes three RPC surfaces. Pick one:

### a) XML-RPC (universal, easy)
Standard Odoo external API. Works with any Odoo instance.

```javascript
// Login (returns uid)
const uid = await xmlrpc('/xmlrpc/2/common', 'authenticate', [
  'wakecap', 'your_email@wakecap.com', 'your_password_or_api_key', {}
]);

// Then call any model:
const result = await xmlrpc('/xmlrpc/2/object', 'execute_kw', [
  'wakecap', uid, 'your_password_or_api_key',
  'stock.picking', 'search_read',
  [[['location_dest_id', 'child_of', 353]]],
  { fields: ['name', 'state', 'scheduled_date'], limit: 100 }
]);
```

For a browser-side dashboard, XML-RPC needs a small proxy since browsers don't do XML-RPC
directly. Node/Python proxies are ~40 lines.

### b) JSON-RPC (browser-friendly)
Same models, over JSON. This is what Odoo's own web client uses.

```javascript
async function odooCall(model, method, args = [], kwargs = {}) {
  const r = await fetch('https://wakecap.odoo.com/web/dataset/call_kw', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',   // relies on session cookie from a prior /web/session/authenticate
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'call',
      params: { model, method, args, kwargs }
    })
  });
  const j = await r.json();
  if (j.error) throw new Error(j.error.data?.message || j.error.message);
  return j.result;
}
```

You'll authenticate once via `/web/session/authenticate` (POST with `db`, `login`, `password`) and the session cookie carries subsequent calls.

### c) OAuth on `odoo.sh` / API keys
For headless integrations Odoo 18 supports personal API keys under
*Preferences → Account Security → New API Key*. Use the API key in place of a password
against either RPC surface. This is the recommended approach for a production dashboard.

---

## 3. Field-by-field mapping

### 3.1 `ODOO_DATA.warehouses` — from `stock.warehouse`

```javascript
warehouses = await odooCall('stock.warehouse', 'search_read',
  [[]],
  { fields: ['id', 'name', 'code'] }
);
```

### 3.2 `ODOO_DATA.stores` — from `stock.location`

For a single project (Riyadh Refinery), fetch the project view location and its children:

```javascript
const projectLocId = 353;   // Riyadh | ARAMCO Riyadh Refinery SRU – Technip
const locations = await odooCall('stock.location', 'search_read',
  [[['id', '=', projectLocId], '|', ['parent_id', '=', projectLocId]]],
  { fields: ['id', 'name', 'usage', 'parent_id', 'quant_ids'] }
);

// Match to Physical / Installation / Quarantine by keyword — naming is inconsistent
// across projects. See docs/schema-reference.md § 1.
const categoryOf = name => {
  const n = name.toLowerCase();
  if (/physical|actual/.test(n)) return 'Physical';
  if (/installation/.test(n))    return 'Installation';
  if (/quarantine/.test(n))      return 'Quarantine';
  return null;
};
```

For the multi-project dashboard, filter `[['usage', '=', 'view'], ['parent_id', '=', false]]`
to get all 87 project view locations at once.

### 3.3 `ODOO_DATA.items` — from `stock.quant` cross-tabulated

This is the heart of the Stores & Items master grid. Per SKU, get on-hand at each of the
3 child locations plus PO Qty and Delivered from the incoming pipeline.

```javascript
// A. On-hand per (product, location) — one call for the whole project subtree
const quants = await odooCall('stock.quant', 'read_group',
  [[
    ['location_id', 'child_of', 353],
    ['location_id', '!=', 353]   // exclude the view location itself
  ]],
  {
    fields: ['product_id', 'location_id', 'quantity:sum'],
    groupby: ['product_id', 'location_id'],
    lazy: false
  }
);

// B. Product master (code + name + category)
const productIds = [...new Set(quants.map(q => q.product_id[0]))];
const products = await odooCall('product.product', 'read',
  [productIds],
  { fields: ['id', 'default_code', 'display_name', 'categ_id', 'uom_id'] }
);

// C. Pivot: one row per SKU with phys/inst/quar columns
const storeCategory = {
  354: 'phys',   // Physical
  355: 'inst',   // Installation
  356: 'quar'    // Quarantine
};
const byProduct = {};
quants.forEach(q => {
  const pid = q.product_id[0];
  byProduct[pid] = byProduct[pid] || { phys: 0, inst: 0, quar: 0 };
  const col = storeCategory[q.location_id[0]];
  if (col) byProduct[pid][col] += q.quantity;
});

// D. PO Qty + Delivered — see § 3.4 below

// E. Emit rows in the schema the dashboard expects
const items = products.map(p => {
  const q = byProduct[p.id] || { phys: 0, inst: 0, quar: 0 };
  const po  = poByProduct[p.id]  || 0;
  const del = delByProduct[p.id] || 0;
  const total = q.phys + q.inst + q.quar;
  return {
    code: p.default_code,
    name: p.display_name,
    cat:  categoryGroup(p.categ_id[1]),   // roll up product.category → dashboard group
    po, del,
    phys: q.phys, inst: q.inst, quar: q.quar,
    total, diff: po - del
  };
});
```

### 3.4 PO Qty + Delivered

**Note:** the Purchase module is not installed on `wakecap.odoo.com`; there is no
`purchase.order` model. The closest proxy is incoming receipts (`stock.picking` with
`picking_type_id.code = 'incoming'`), which is what the reference Excel sheet uses.

```javascript
const receipts = await odooCall('stock.picking', 'search_read',
  [[
    ['location_dest_id', 'child_of', 353],
    ['picking_type_code', '=', 'incoming']
  ]],
  { fields: ['id', 'state', 'move_ids'] }
);

const moveIds = receipts.flatMap(r => r.move_ids);
const moves = await odooCall('stock.move', 'read',
  [moveIds],
  { fields: ['product_id', 'product_uom_qty', 'quantity_done', 'state'] }
);

const poByProduct = {}, delByProduct = {};
moves.forEach(m => {
  const pid = m.product_id[0];
  poByProduct[pid]  = (poByProduct[pid]  || 0) + m.product_uom_qty;
  if (m.state === 'done')
    delByProduct[pid] = (delByProduct[pid] || 0) + m.quantity_done;
});
```

If you later install the Purchase module, replace this with
`purchase.order.line.read_group` grouped by `product_id`.

### 3.5 `ODOO_DATA.stock_picking` — Material Vouchers table

```javascript
const pickings = await odooCall('stock.picking', 'search_read',
  [[['location_dest_id', 'child_of', 353]]],
  {
    fields: [
      'name', 'state',
      'location_id', 'location_dest_id',
      'picking_type_id', 'scheduled_date', 'date_done',
      'origin', 'partner_id',
      'x_studio_receipt_type', 'x_studio_status_receipt_order',
      'x_studio_request_manager', 'x_studio_delivery_date',
      'x_studio_dn_', 'x_studio_rev'
    ],
    order: 'scheduled_date desc',
    limit: 500
  }
);

// The dashboard's UI uses a 'project_id' field on each picking that Odoo does NOT
// natively provide — derive it from the destination location's parent view:
pickings.forEach(p => {
  p.project_id = viewParentOf(p.location_dest_id[0]);
});
```

### 3.6 `ODOO_DATA.receipt_lines` — demand vs received

Filter step 3.5's result to `picking_type_code === 'incoming'` and `state === 'done'`, then
read `stock.move` for each with the `product_uom_qty` (demand) and `quantity_done` (received).

### 3.7 `ODOO_DATA.license` — no Odoo source yet

The subscription / license module (`sale_subscription · contract · license · sla · renewal`)
is not installed. This panel is UI-only and clearly labelled as such. To wire it up, add
either the Odoo Subscriptions app, a Studio custom model, or fetch from an external SaaS
licence server.

---

## 4. Making it a live dashboard

The current HTML is a snapshot. For a live view, wrap the `ODOO_DATA` block in an
async loader:

```javascript
// Replace the const ODOO_DATA = { ... }; block with:
let ODOO_DATA = null;

async function loadOdoo() {
  ODOO_DATA = {
    warehouses:     await fetchWarehouses(),
    stores:         await fetchStores(353),
    product_categories: await fetchCategories(),
    items:          await fetchItems(353),
    stock_picking:  await fetchPickings(353),
    receipt_lines:  await fetchReceiptLines(353),
    license:        await fetchLicense(353)   // placeholder — see § 3.7
  };
}

window.addEventListener('DOMContentLoaded', async () => {
  const status = document.querySelector('#page-title');
  try {
    status.textContent = 'Loading from Odoo…';
    await loadOdoo();
    status.textContent = 'Operations Overview';
    boot();   // move the current IIFE body into a boot() function
  } catch (e) {
    status.textContent = 'Failed to load Odoo data — see console';
    console.error(e);
  }
});
```

Add an "as of" timestamp so users know how fresh the data is, and a refresh button
that re-runs `loadOdoo()`.

---

## 5. CORS & deployment

`wakecap.odoo.com` doesn't set CORS headers for arbitrary origins by default. Options:

- **Host the dashboard under the same origin** — serve it from an Odoo controller
  (`http.Controller` returning the HTML) so `credentials: 'include'` works with the session cookie.
- **Small proxy** — put a Node/Cloudflare Worker in front that adds `Access-Control-Allow-*`
  headers and forwards JSON-RPC. This is the fastest path to a standalone dashboard.
- **`odoo.sh` custom module** — publish a custom module that exposes the required data as
  a public JSON endpoint (with API-key auth).

---

## 6. Access grants to request from the Odoo admin

To unlock everything the dashboard currently marks as restricted/unavailable:

- **Valuation access** — `stock_account` is installed, but `stock.quant.value` is denied
  for the current login. Grant "Stock Valuation" access to see monetary value per store.
- **Purchase module** — install to enable a real PO-ordered-vs-received view.
- **Subscriptions or a Studio license model** — for the License panel to move from
  placeholder to real.

---

## 7. Testing your integration

The reference Excel (`data/riyadh-sru-inventory.xlsx`) is the June 2026 ground-truth pull.
After wiring up your integration, load the Riyadh Refinery dashboard and check that:

- Physical store shows **7,508** on-hand units across **125 SKUs**
- Installation store shows **62** units across **9 SKUs**
- Quarantine store shows **0**
- MV status: **79 delivered · 41 cancelled · 2 draft · 1 ready**
- The `TR-CCTV-ATEX-001` row shows Physical = **−18** (real inventory anomaly — keep it visible, don't sanitize)

If those numbers match, the wiring is correct.
