From 3db8a14fab0f101fab0db2af6e576448ea716378 Mon Sep 17 00:00:00 2001 From: Kristof Bernaert Date: Sun, 14 Jun 2026 13:31:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20refactor=20dlv=5Ffleetflow=5Fcontactflo?= =?UTF-8?q?w=20=E2=80=94=20add=20customers/suppliers=20endpoints,=20settin?= =?UTF-8?q?gs=20tab,=20make=20busenco=5Fcustom=20optional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dlv_fleetflow_contactflow/CHANGELOG.md | 19 ++- dlv_fleetflow_contactflow/README.md | 127 ++++++---------- dlv_fleetflow_contactflow/__manifest__.py | 10 +- dlv_fleetflow_contactflow/controllers/main.py | 137 +++++++++++++++--- dlv_fleetflow_contactflow/models/__init__.py | 1 + .../models/res_config_settings.py | 22 +++ .../views/res_config_settings_views.xml | 74 ++++++++++ 7 files changed, 286 insertions(+), 104 deletions(-) create mode 100644 dlv_fleetflow_contactflow/models/res_config_settings.py create mode 100644 dlv_fleetflow_contactflow/views/res_config_settings_views.xml diff --git a/dlv_fleetflow_contactflow/CHANGELOG.md b/dlv_fleetflow_contactflow/CHANGELOG.md index 7ebde54..e41794d 100644 --- a/dlv_fleetflow_contactflow/CHANGELOG.md +++ b/dlv_fleetflow_contactflow/CHANGELOG.md @@ -5,10 +5,23 @@ 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.1.0] - 2026-06-14 + +### Added +- `GET /fleetflow/contactflow/customers` endpoint (`res.partner`, `customer_rank > 0`, requires phone or mobile). +- `GET /fleetflow/contactflow/suppliers` endpoint (`res.partner`, `supplier_rank > 0`, requires phone or mobile). +- FleetFlow Settings tab (ContactFlow API) showing endpoint status, authentication instructions, and response format. +- Live `busenco_custom` install check — a warning is shown in Settings when the drivers endpoint is unavailable. + +### Changed +- Removed `busenco_custom` from hard `depends` — it is now optional at runtime. +- Added `dlv_fleetflow_base` to `depends`. +- Drivers endpoint returns a 404 JSON error when `busenco_custom` is not installed. + ## [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`. +- `GET /fleetflow/contactflow/drivers?location=` endpoint. +- Bearer token authentication via Odoo native API keys. +- Optional `location` filter on the drivers endpoint. diff --git a/dlv_fleetflow_contactflow/README.md b/dlv_fleetflow_contactflow/README.md index be12488..fa71e1c 100644 --- a/dlv_fleetflow_contactflow/README.md +++ b/dlv_fleetflow_contactflow/README.md @@ -2,60 +2,43 @@ ## 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. +`dlv_fleetflow_contactflow` is a generic contact sync feed addon. It provides +token-authenticated JSON endpoints consumed by the WordPress +`dlv-odoo-contactflow` plugin to keep an external contact list in sync with +Odoo. -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=""` +The **customers** and **suppliers** endpoints (backed by `res.partner`) are +always available. The **drivers** endpoint requires the `busenco_custom` addon +(which provides the `trip.driver` model); when `busenco_custom` is not +installed, the drivers endpoint responds with a 404 JSON error and the FleetFlow +Settings tab shows a warning. ## Dependencies - `base` -- `busenco_custom` (provides the `trip.driver` model) +- `base_setup` +- `dlv_fleetflow_base` +- `busenco_custom` *(optional — only needed for the drivers endpoint)* -## Endpoint +## Endpoints -### URL +| Endpoint | Requires | +|----------|----------| +| `GET /fleetflow/contactflow/customers` | always active | +| `GET /fleetflow/contactflow/suppliers` | always active | +| `GET /fleetflow/contactflow/drivers?location=X` | `busenco_custom` installed | -``` -GET /fleetflow/contactflow/drivers -``` +## Authentication -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**. +Bearer token using Odoo's native API keys: ``` Authorization: Bearer ``` -The key is validated against `res.users.apikeys` with the `rpc` scope. A missing, -malformed, or invalid key returns **401 Unauthorized**. +Generate a key via **Settings → Users → (user) → API Keys → New API Key**. -Create a key in Odoo: **Settings → Users → (select user) → Account Security → -API Keys → New API Key**. - -### Response format +## Response format `Content-Type: application/json;charset=utf-8` @@ -74,53 +57,37 @@ API Keys → New API Key**. } ``` -On error the endpoint returns **500** with: +## Name splitting -```json -{ "error": "" } -``` +Contacts expose separate `first_name` / `last_name` fields, derived from a +single source name field: -### Field mapping +- **People** (drivers, individual partners): the name is split on the **first + space** — `"Jan Van den Berg"` → `first_name="Jan"`, `last_name="Van den Berg"`. +- **Companies** (`is_company = True`): `first_name` is empty and `last_name` + holds the full company name. -| 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 | +## Settings -Only drivers with `active = True` are returned, ordered by `name` ascending. +A **ContactFlow API** tab is available under **Settings → FleetFlow**. It shows +the live status of each endpoint, authentication instructions, and the response +format. When `busenco_custom` is installed the drivers endpoint is reported as +active; otherwise a warning explains how to enable it. + +## Installation + +1. Install `dlv_fleetflow_base` first (it owns the FleetFlow Settings entry). +2. Install `dlv_fleetflow_contactflow`. +3. *(Optional)* Install `busenco_custom` to enable the drivers endpoint. +4. Create an API key for the user that should own the feed + (**Settings → Users → API Keys → New API Key**). ## 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. +In the `dlv-odoo-contactflow` WordPress plugin, configure a sync profile with: -## Installation steps +- the endpoint URL (e.g. `https:///fleetflow/contactflow/customers`), +- the Bearer token generated above. -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" - ``` +The plugin polls the endpoint and maps the returned `contacts` array into its +ContactFlow records. diff --git a/dlv_fleetflow_contactflow/__manifest__.py b/dlv_fleetflow_contactflow/__manifest__.py index f3d076f..a34e189 100644 --- a/dlv_fleetflow_contactflow/__manifest__.py +++ b/dlv_fleetflow_contactflow/__manifest__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- { "name": "FleetFlow ContactFlow API", - "summary": "Expose Busenco trip drivers as a ContactFlow JSON API " - "secured by an API key.", + "summary": "Generic contact sync feed endpoints for WordPress " + "ContactFlow integration", 'description': """ Exposes trip.driver records as a JSON feed endpoint, consumed by the WordPress dlv-odoo-contactflow plugin. @@ -17,8 +17,10 @@ Generate a key via Settings > Users > (user) > API Keys > New. "category": "Technical", "version": "16.0.1.0.0", "license": "LGPL-3", - "depends": ["base", "busenco_custom"], - "data": [], + "depends": ["base", "base_setup", "dlv_fleetflow_base"], + "data": [ + "views/res_config_settings_views.xml", + ], "installable": True, "application": False, "auto_install": False, diff --git a/dlv_fleetflow_contactflow/controllers/main.py b/dlv_fleetflow_contactflow/controllers/main.py index ee7389e..e753ed7 100644 --- a/dlv_fleetflow_contactflow/controllers/main.py +++ b/dlv_fleetflow_contactflow/controllers/main.py @@ -10,6 +10,35 @@ _logger = logging.getLogger(__name__) class FleetFlowContactFlowController(http.Controller): + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + @staticmethod + def _json_response(payload, status=200): + """Return a JSON HTTP response with the ContactFlow content type.""" + return request.make_response( + json.dumps(payload), + headers=[("Content-Type", "application/json;charset=utf-8")], + status=status, + ) + + @staticmethod + def _busenco_installed(): + """Return True when the busenco_custom module is installed.""" + return request.env["ir.module.module"].sudo().search_count([ + ("name", "=", "busenco_custom"), + ("state", "=", "installed"), + ]) > 0 + + @staticmethod + def _split_name(name): + """Split a free-text name on the first space into (first, last).""" + first_name, _sep, last_name = (name or "").partition(" ") + return first_name, last_name + + # ------------------------------------------------------------------ + # Drivers (requires busenco_custom) + # ------------------------------------------------------------------ @http.route( "/fleetflow/contactflow/drivers", type="http", @@ -17,12 +46,21 @@ class FleetFlowContactFlowController(http.Controller): methods=["GET"], csrf=False, ) - def fleetflow_contactflow_drivers(self, location=None, **kwargs): + def get_drivers(self, location=None, **kwargs): """Return active trip drivers as ContactFlow JSON contacts. Optional query param ``location`` filters drivers on - ``location_id.name``. + ``location_id.name``. Returns 404 when busenco_custom is not installed. """ + if not self._busenco_installed(): + return self._json_response( + { + "error": "busenco_custom module is not installed. " + "The drivers endpoint is unavailable." + }, + status=404, + ) + try: domain = [("active", "=", True)] if location: @@ -34,8 +72,7 @@ class FleetFlowContactFlowController(http.Controller): contacts = [] for d in drivers: - name = d.name or "" - first_name, sep, last_name = name.partition(" ") + first_name, last_name = self._split_name(d.name) contacts.append({ "id": str(d.id), "first_name": first_name, @@ -50,18 +87,84 @@ class FleetFlowContactFlowController(http.Controller): len(contacts), location, ) - return request.make_response( - json.dumps({"contacts": contacts}), - headers=[ - ("Content-Type", "application/json;charset=utf-8"), - ], - ) + return self._json_response({"contacts": contacts}) 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, + _logger.exception("FleetFlow ContactFlow: error building drivers response") + return self._json_response({"error": str(exc)}, status=500) + + # ------------------------------------------------------------------ + # Customers + # ------------------------------------------------------------------ + @http.route( + "/fleetflow/contactflow/customers", + type="http", + auth="fleetflow_api_key", + methods=["GET"], + csrf=False, + ) + def get_customers(self, **kwargs): + """Return active customer partners (with a phone or mobile) as contacts.""" + return self._partner_feed( + rank_field="customer_rank", label="customers" + ) + + # ------------------------------------------------------------------ + # Suppliers + # ------------------------------------------------------------------ + @http.route( + "/fleetflow/contactflow/suppliers", + type="http", + auth="fleetflow_api_key", + methods=["GET"], + csrf=False, + ) + def get_suppliers(self, **kwargs): + """Return active supplier partners (with a phone or mobile) as contacts.""" + return self._partner_feed( + rank_field="supplier_rank", label="suppliers" + ) + + # ------------------------------------------------------------------ + # Shared res.partner feed + # ------------------------------------------------------------------ + def _partner_feed(self, rank_field, label): + """Build a ContactFlow feed from res.partner filtered by rank field.""" + try: + domain = [ + ("active", "=", True), + (rank_field, ">", 0), + "|", + ("phone", "!=", False), + ("mobile", "!=", False), + ] + + partners = request.env["res.partner"].sudo().search( + domain, order="name asc" ) + + contacts = [] + for p in partners: + if p.is_company: + first_name = "" + last_name = p.name or "" + else: + first_name, last_name = self._split_name(p.name) + contacts.append({ + "id": str(p.id), + "first_name": first_name, + "last_name": last_name, + "phone": p.phone or "", + "mobile": p.mobile or "", + "email": p.email or "", + }) + + _logger.info( + "FleetFlow ContactFlow: returned %s %s", len(contacts), label + ) + + return self._json_response({"contacts": contacts}) + except Exception as exc: + _logger.exception( + "FleetFlow ContactFlow: error building %s response", label + ) + return self._json_response({"error": str(exc)}, status=500) diff --git a/dlv_fleetflow_contactflow/models/__init__.py b/dlv_fleetflow_contactflow/models/__init__.py index 672127f..e6237d1 100644 --- a/dlv_fleetflow_contactflow/models/__init__.py +++ b/dlv_fleetflow_contactflow/models/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- from . import ir_http +from . import res_config_settings diff --git a/dlv_fleetflow_contactflow/models/res_config_settings.py b/dlv_fleetflow_contactflow/models/res_config_settings.py new file mode 100644 index 0000000..369564e --- /dev/null +++ b/dlv_fleetflow_contactflow/models/res_config_settings.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + fleetflow_busenco_installed = fields.Boolean( + string="busenco_custom installed", + compute="_compute_fleetflow_busenco_installed", + readonly=True, + store=False, + ) + + @api.depends_context("uid") + def _compute_fleetflow_busenco_installed(self): + installed = self.env["ir.module.module"].sudo().search_count([ + ("name", "=", "busenco_custom"), + ("state", "=", "installed"), + ]) > 0 + for rec in self: + rec.fleetflow_busenco_installed = installed diff --git a/dlv_fleetflow_contactflow/views/res_config_settings_views.xml b/dlv_fleetflow_contactflow/views/res_config_settings_views.xml new file mode 100644 index 0000000..777388c --- /dev/null +++ b/dlv_fleetflow_contactflow/views/res_config_settings_views.xml @@ -0,0 +1,74 @@ + + + + + res.config.settings.view.form.inherit.fleetflow.contactflow + res.config.settings + + + + + + +
+
+
+ + Available Endpoints +
+ + + + + + + + + + + + + + + + + + + + + +
EndpointStatus
/fleetflow/contactflow/customersActive
/fleetflow/contactflow/suppliersActive
/fleetflow/contactflow/drivers?location=Xsee below
+
+ + Bus Drivers Endpoint + + + + Authentication +
+ Use a Bearer token. Generate via + Settings > Users > (user) > API Keys > New API Key +
+ + Response format +
+ { "contacts": [ { "id", "first_name", "last_name", "phone", "mobile", "email" } ] } +
+ +
+
+
+
+
+
+
+ +