feat: add dlv_fleetflow_contactflow addon + ignore .DS_Store
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-06-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release.
|
||||||
|
- `GET /fleetflow/contactflow/drivers` endpoint returning active trip drivers as JSON contacts.
|
||||||
|
- Bearer API key authentication via Odoo native API keys (`res.users.apikeys`, `rpc` scope).
|
||||||
|
- Optional `location` query parameter to filter drivers on `location_id.name`.
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# dlv_fleetflow_contactflow
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
FleetFlow ContactFlow API exposes the active trip drivers managed by the
|
||||||
|
`busenco_custom` addon (`trip.driver`) over a token-authenticated HTTP endpoint
|
||||||
|
that returns JSON contacts. It is designed to feed external systems — such as a
|
||||||
|
WordPress ContactFlow integration — with an always-current driver contact list,
|
||||||
|
optionally filtered by office location.
|
||||||
|
|
||||||
|
Driver names are stored as a single `name` field on `trip.driver`. The endpoint
|
||||||
|
splits `trip.driver.name` on the **first space** into `first_name` and
|
||||||
|
`last_name`:
|
||||||
|
|
||||||
|
- `"Jan Peeters"` → `first_name="Jan"`, `last_name="Peeters"`
|
||||||
|
- `"Jan Van den Berg"` → `first_name="Jan"`, `last_name="Van den Berg"`
|
||||||
|
- `"Jan"` (no space) → `first_name="Jan"`, `last_name=""`
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `base`
|
||||||
|
- `busenco_custom` (provides the `trip.driver` model)
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
### URL
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /fleetflow/contactflow/drivers
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional query parameter:
|
||||||
|
|
||||||
|
| Param | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `location` | Filter drivers on `location_id.name` (exact match) |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /fleetflow/contactflow/drivers?location=Antwerpen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
Bearer token using Odoo's **native API keys**.
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <your_api_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
The key is validated against `res.users.apikeys` with the `rpc` scope. A missing,
|
||||||
|
malformed, or invalid key returns **401 Unauthorized**.
|
||||||
|
|
||||||
|
Create a key in Odoo: **Settings → Users → (select user) → Account Security →
|
||||||
|
API Keys → New API Key**.
|
||||||
|
|
||||||
|
### Response format
|
||||||
|
|
||||||
|
`Content-Type: application/json;charset=utf-8`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"contacts": [
|
||||||
|
{
|
||||||
|
"id": "42",
|
||||||
|
"first_name": "Jan",
|
||||||
|
"last_name": "Peeters",
|
||||||
|
"phone": "+3231234567",
|
||||||
|
"mobile": "+32475123456",
|
||||||
|
"email": "jan.peeters@example.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On error the endpoint returns **500** with:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "error": "<message>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field mapping
|
||||||
|
|
||||||
|
| JSON field | Source (`trip.driver`) | Notes |
|
||||||
|
|--------------|-------------------------------|-------|
|
||||||
|
| `id` | `id` | Cast to string |
|
||||||
|
| `first_name` | `name` (before first space) | Split on first space |
|
||||||
|
| `last_name` | `name` (after first space) | Remainder after first space; `""` if none |
|
||||||
|
| `phone` | `phone_number` | `""` if empty |
|
||||||
|
| `mobile` | `phone_number2` | `""` if empty |
|
||||||
|
| `email` | `email` | `""` if empty |
|
||||||
|
|
||||||
|
Only drivers with `active = True` are returned, ordered by `name` ascending.
|
||||||
|
|
||||||
|
## WordPress setup
|
||||||
|
|
||||||
|
1. Store the Odoo base URL and the API key in your WordPress configuration
|
||||||
|
(e.g. as constants in `wp-config.php` or via your integration plugin's
|
||||||
|
settings), never hard-coded in templates.
|
||||||
|
2. Make a server-side `GET` request to
|
||||||
|
`https://<odoo-host>/fleetflow/contactflow/drivers` with the header
|
||||||
|
`Authorization: Bearer <your_api_key>`.
|
||||||
|
3. Add the `location` query parameter if you only want drivers for a specific
|
||||||
|
office location.
|
||||||
|
4. Parse the `contacts` array from the JSON response and render it through your
|
||||||
|
ContactFlow templates.
|
||||||
|
5. Cache the response (e.g. a transient) to avoid polling Odoo on every page
|
||||||
|
load.
|
||||||
|
|
||||||
|
## Installation steps
|
||||||
|
|
||||||
|
1. Copy the `dlv_fleetflow_contactflow` directory into your Odoo addons path
|
||||||
|
(alongside `busenco_custom`).
|
||||||
|
2. Restart the Odoo server.
|
||||||
|
3. Enable **Developer Mode** and update the apps list
|
||||||
|
(**Apps → Update Apps List**).
|
||||||
|
4. Search for **FleetFlow ContactFlow API** and click **Install**.
|
||||||
|
5. Create an API key for the user that should own the feed
|
||||||
|
(**Settings → Users → Account Security → API Keys → New API Key**).
|
||||||
|
6. Test the endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <your_api_key>" \
|
||||||
|
"https://<odoo-host>/fleetflow/contactflow/drivers"
|
||||||
|
```
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
Planned extensions to the FleetFlow ContactFlow API addon. Items are grouped by
|
||||||
|
target release and are subject to change.
|
||||||
|
|
||||||
|
## v1.1 — Additional feed types (buses, locations)
|
||||||
|
|
||||||
|
- Add `GET /fleetflow/contactflow/buses` exposing `trip.bus` records.
|
||||||
|
- Add `GET /fleetflow/contactflow/locations` exposing `office.trip.location` records.
|
||||||
|
- Share the authentication and JSON-response plumbing across all feed types.
|
||||||
|
|
||||||
|
## v1.2 — Per-category field selection
|
||||||
|
|
||||||
|
- Support a `fields` query parameter so callers receive only the fields they
|
||||||
|
request (e.g. `?fields=first_name,last_name,email`).
|
||||||
|
- Reduce payload size and decouple consumers from the full schema.
|
||||||
|
|
||||||
|
## v2.0 — Rename busenco_custom to fleetflow
|
||||||
|
|
||||||
|
- Migrate the dependency from `busenco_custom` to the renamed `fleetflow` addon.
|
||||||
|
- Update model references and the manifest `depends` accordingly.
|
||||||
|
- Provide a migration path for existing installations.
|
||||||
|
|
||||||
|
## Future — Webhook push on driver change
|
||||||
|
|
||||||
|
- Emit a webhook to subscribed endpoints when a driver record changes, instead
|
||||||
|
of relying on consumers polling the feed.
|
||||||
|
- Allow registration and management of webhook subscriptions.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
"name": "FleetFlow ContactFlow API",
|
||||||
|
"summary": "Expose Busenco trip drivers as a ContactFlow JSON API "
|
||||||
|
"secured by an API key.",
|
||||||
|
"description": """
|
||||||
|
FleetFlow ContactFlow API
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Provides a token-authenticated HTTP endpoint that returns the active
|
||||||
|
trip drivers (busenco_custom ``trip.driver``) as JSON contacts,
|
||||||
|
optionally filtered by office location.
|
||||||
|
""",
|
||||||
|
"author": "bv Domus La Vila",
|
||||||
|
"website": "https://domuslavila.eu",
|
||||||
|
"category": "Technical",
|
||||||
|
"version": "16.0.1.0.0",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"depends": ["base", "busenco_custom"],
|
||||||
|
"data": [],
|
||||||
|
"installable": True,
|
||||||
|
"application": False,
|
||||||
|
"auto_install": False,
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import main
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FleetFlowContactFlowController(http.Controller):
|
||||||
|
|
||||||
|
@http.route(
|
||||||
|
"/fleetflow/contactflow/drivers",
|
||||||
|
type="http",
|
||||||
|
auth="fleetflow_api_key",
|
||||||
|
methods=["GET"],
|
||||||
|
csrf=False,
|
||||||
|
)
|
||||||
|
def fleetflow_contactflow_drivers(self, location=None, **kwargs):
|
||||||
|
"""Return active trip drivers as ContactFlow JSON contacts.
|
||||||
|
|
||||||
|
Optional query param ``location`` filters drivers on
|
||||||
|
``location_id.name``.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
domain = [("active", "=", True)]
|
||||||
|
if location:
|
||||||
|
domain.append(("location_id.name", "=", location))
|
||||||
|
|
||||||
|
drivers = request.env["trip.driver"].sudo().search(
|
||||||
|
domain, order="name asc"
|
||||||
|
)
|
||||||
|
|
||||||
|
contacts = []
|
||||||
|
for d in drivers:
|
||||||
|
name = d.name or ""
|
||||||
|
first_name, sep, last_name = name.partition(" ")
|
||||||
|
contacts.append({
|
||||||
|
"id": str(d.id),
|
||||||
|
"first_name": first_name,
|
||||||
|
"last_name": last_name,
|
||||||
|
"phone": d.phone_number or "",
|
||||||
|
"mobile": d.phone_number2 or "",
|
||||||
|
"email": d.email or "",
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"FleetFlow ContactFlow: returned %s drivers (location=%s)",
|
||||||
|
len(contacts), location,
|
||||||
|
)
|
||||||
|
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({"contacts": contacts}),
|
||||||
|
headers=[
|
||||||
|
("Content-Type", "application/json;charset=utf-8"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.exception("FleetFlow ContactFlow: error building response")
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps({"error": str(exc)}),
|
||||||
|
headers=[
|
||||||
|
("Content-Type", "application/json;charset=utf-8"),
|
||||||
|
],
|
||||||
|
status=500,
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import ir_http
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.exceptions import AccessDenied
|
||||||
|
from werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
_inherit = "ir.http"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _auth_method_fleetflow_api_key(cls):
|
||||||
|
"""Authenticate a request using a Bearer API key.
|
||||||
|
|
||||||
|
The caller must provide an ``Authorization: Bearer <key>`` header.
|
||||||
|
The key is validated against ``res.users.apikeys`` with the ``rpc``
|
||||||
|
scope. On success ``request.uid`` is set to the owning user; on
|
||||||
|
failure a 401 Unauthorized is raised.
|
||||||
|
"""
|
||||||
|
authorization = request.httprequest.headers.get("Authorization", "")
|
||||||
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
|
_logger.warning(
|
||||||
|
"FleetFlow API: missing or malformed Authorization header"
|
||||||
|
)
|
||||||
|
raise Unauthorized("Missing or invalid Authorization header")
|
||||||
|
|
||||||
|
api_key = authorization[len("Bearer "):].strip()
|
||||||
|
if not api_key:
|
||||||
|
raise Unauthorized("Empty API key")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = request.env["res.users.apikeys"]._check_credentials(
|
||||||
|
scope="rpc", key=api_key
|
||||||
|
)
|
||||||
|
except AccessDenied:
|
||||||
|
user_id = False
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
_logger.warning("FleetFlow API: invalid API key")
|
||||||
|
raise Unauthorized("Invalid API key")
|
||||||
|
|
||||||
|
request.uid = user_id
|
||||||
Reference in New Issue
Block a user