From dfcc54829561c891cf48097bceb8274bb350b6ce Mon Sep 17 00:00:00 2001 From: Kristof Bernaert Date: Sun, 14 Jun 2026 01:00:47 +0200 Subject: [PATCH] feat: add dlv_fleetflow_contactflow addon + ignore .DS_Store --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 1 + dlv_fleetflow_contactflow/CHANGELOG.md | 14 ++ dlv_fleetflow_contactflow/README.md | 126 ++++++++++++++++++ dlv_fleetflow_contactflow/ROADMAP.md | 28 ++++ dlv_fleetflow_contactflow/__init__.py | 3 + dlv_fleetflow_contactflow/__manifest__.py | 24 ++++ .../controllers/__init__.py | 2 + dlv_fleetflow_contactflow/controllers/main.py | 67 ++++++++++ dlv_fleetflow_contactflow/models/__init__.py | 2 + dlv_fleetflow_contactflow/models/ir_http.py | 46 +++++++ 11 files changed, 313 insertions(+) delete mode 100644 .DS_Store create mode 100644 dlv_fleetflow_contactflow/CHANGELOG.md create mode 100644 dlv_fleetflow_contactflow/README.md create mode 100644 dlv_fleetflow_contactflow/ROADMAP.md create mode 100644 dlv_fleetflow_contactflow/__init__.py create mode 100644 dlv_fleetflow_contactflow/__manifest__.py create mode 100644 dlv_fleetflow_contactflow/controllers/__init__.py create mode 100644 dlv_fleetflow_contactflow/controllers/main.py create mode 100644 dlv_fleetflow_contactflow/models/__init__.py create mode 100644 dlv_fleetflow_contactflow/models/ir_http.py diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index dbee9771ef48bb72ac03bb21e0867132fde854d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6u#e5=**PT3$!{c78a_gw7`}Y`G@Tf5dNF~(DLWHyEDSTbf)gi?iQQI zCK@ybd;??D=nEle!h?~B(Fchyn)sqt6d!o;1&vV?jl}rkxpSut6nH2mig9i-_sqHX zoICe?`<=NxQ^put3q~VjI%7>8(Qwe{ zp6Ym`~$_)E9(Jw&V?DZ8u9^A7lq?%kz`T z%ZN=Wn>ww$TrICCKiGBHEp_=Bzu>iI^81C>?Yee$>^_s-W0|EiuWoZ4KVvy&K5(!M zlS~fuT8`nCTHS(Uc!6)1oKRIoP4l@!LrWT)o0AQR#>Q2{4T+(~rj?|uYFa%!tSU8i z%hqR@yz_Tdw2nG4meDx>Qo)p`M>!6Q5uA$dctN*R^ck zwrjLW*(cX_@)G882r#hUFaUDOrYpxVLC|R@OGVolL)}^0Vq_Wz`IR z-~GDYSIAmV6)gW4uTzv@skwTscEF&0>~syWn4$0sWKR!C=(YNOG1HbesW(E02lYi- zw?|PaJR=Ps)#|nGqr?i8dvau{mehOfOxCnjZph~}X)E;Jq6|*u+vVUYZH?ZSbNl-# z5>K6p206T5->5$>p1dRD`MU>9N9^j7oNLwF^?lyqOy1mGB4R516FI(x#)F@SW`x@& z!}8s{*fWK`y;j%viUnH?BM5|7&8XBn^iG9;q0moaIYo=ofpC{uq12@oOZX*()lwPn z#X&fSEo9AX9qV9w>0?@nonWWfX?BL4XYaF**{AGtcA0(6uCQ;}ckD;@6Z@I{!v4TC zltYDv*_e+67GN=!A&KQ^!aB5|746uBUFb#+deMgrEF6K2eiZQxM(`}2!%4h`*YO74 z#5uf+3wRF~aS2~y6kp*guHk$9fM4+&e#iBg7ORL=$L7T1a_6zJTE>M9?<%xWC&;-a9{!- zM=6`;#I*-%$8v3A4)#z!n1hNSH7^gX7c5jLANbTzUGy-i+xax9#_Jmt$~_(nR4txt zR4K#wlu)f-K`Nx8tzM&2uJN)^-LR3=vz!HLJl@t$3ti&M-Lv={dxu?QAFGuT7CV z)J6Tj>CWH(@A){Q4Mqq=2;4maC~NO(Zzcaby~jkYouKOpx +``` + +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": "" } +``` + +### 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:///fleetflow/contactflow/drivers` with the header + `Authorization: Bearer `. +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 " \ + "https:///fleetflow/contactflow/drivers" + ``` diff --git a/dlv_fleetflow_contactflow/ROADMAP.md b/dlv_fleetflow_contactflow/ROADMAP.md new file mode 100644 index 0000000..4033a9a --- /dev/null +++ b/dlv_fleetflow_contactflow/ROADMAP.md @@ -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. diff --git a/dlv_fleetflow_contactflow/__init__.py b/dlv_fleetflow_contactflow/__init__.py new file mode 100644 index 0000000..3b38916 --- /dev/null +++ b/dlv_fleetflow_contactflow/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import models +from . import controllers diff --git a/dlv_fleetflow_contactflow/__manifest__.py b/dlv_fleetflow_contactflow/__manifest__.py new file mode 100644 index 0000000..fad816a --- /dev/null +++ b/dlv_fleetflow_contactflow/__manifest__.py @@ -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, +} diff --git a/dlv_fleetflow_contactflow/controllers/__init__.py b/dlv_fleetflow_contactflow/controllers/__init__.py new file mode 100644 index 0000000..757b12a --- /dev/null +++ b/dlv_fleetflow_contactflow/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/dlv_fleetflow_contactflow/controllers/main.py b/dlv_fleetflow_contactflow/controllers/main.py new file mode 100644 index 0000000..ee7389e --- /dev/null +++ b/dlv_fleetflow_contactflow/controllers/main.py @@ -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, + ) diff --git a/dlv_fleetflow_contactflow/models/__init__.py b/dlv_fleetflow_contactflow/models/__init__.py new file mode 100644 index 0000000..672127f --- /dev/null +++ b/dlv_fleetflow_contactflow/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import ir_http diff --git a/dlv_fleetflow_contactflow/models/ir_http.py b/dlv_fleetflow_contactflow/models/ir_http.py new file mode 100644 index 0000000..b1103c2 --- /dev/null +++ b/dlv_fleetflow_contactflow/models/ir_http.py @@ -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 `` 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