Skip to content
OrtmannConsulting
Back to all projects

Sales & customer service, construction chemicals

Customer article lists — without a second, customer-safe index

Customers assemble their own article lists and export them with the product data their systems need. Stock, prices and internal figures live in the same index — the separation is enforced by a server-side allow-list.

Year
2026
Services
Automation
Area
Construction chemicals
Role
In-house, employed

Delivered in an employed role for an internal department, not under an external consulting contract. The organisation is not named.

81Columns in the largest export layout
10Languages supported
3Source systems combined

The situation

What triggered this is not documented in the project folder. No README at the root, no requirements description, and nine out of ten commit messages read “update”, “add new features” or “fix bug”.

Two things are evidenced. The folder contains a customer-specific article list as a CSV — 1,366 rows, around 70 columns: article number, EANs per packaging unit, minimum order quantities, weights, dimensions, country of origin, dangerous-goods and CLP classification, marketing copy, datasheet filenames. And code comments describe later-added fields explicitly as “datasheet gap fields” — fields that had been missing from exactly such a list.

That such lists were previously assembled by hand from ERP and PIM data in Excel is plausible but not evidenced. What is evidenced is only that the application’s field selection was subsequently aligned to that list.

The approach

A Flask application combining three data sources.

The catalogue comes from two search indexes: ERP master data (one document per article and country, collapsed in the application to one row per article) and product content with multilingual copy and document links, joined by article number. Filters run as facets over the same index and therefore cascade. The article lists live in their own relational database. The purchase history is queried on demand against the data warehouse: delivered order lines, aggregated per article.

Holding it together is customer resolution. It sits in one single function, and every list or history query takes the customer number from there — never from the request. Two modes are designed for: internal (staff pick a customer) and external (identity from the sign-in token). Switching is a change to that one function, not to the 19 routes.

Where it could have gone the other way

The customer-facing catalogue reads the same index that internally also holds stock, inventory value, orders, suppliers and top customers.

A separate, customer-safe index was deliberately not built. Instead the separation is enforced server-side: an explicit allow-list of permitted fields, with the search engine returning only selected fields. Anything not on that list never reaches the client.

That saves a second index and its loading — but moves the responsibility into a list that must be maintained every time a field is added. The code says so itself, and marks one exception as “fine for the internal phase, review before external release”.

How seriously the principle is taken shows in the final commit. Withdrawn documents had until then been included in internal mode, so staff could review them via a toggle. The commit removes the toggle and its interface code and always filters server-side — the review view was given up in favour of a code path where the URL of a withdrawn safety datasheet cannot leave the backend.

The result

Around 3,250 lines of Python across eleven modules plus roughly 1,900 lines of interface, 19 routes, three connected systems across three search indexes and two databases, three export layouts with 74, 75 and 81 columns, ten languages. Just under eight weeks of calendar time, one person.

Deliberately not included: no ordering, no prices, no stock. Lists are lists — no checkout, no write-back path into the ERP. Work continues outside the system.

Honest about status: the external customer login is not live yet. Internal mode is the default, with the claim mapping for phase 2 marked open in the code; until then staff pick the customer manually. “Purchased” is also an approximation — delivered order lines rather than invoices, named in the code as the best available signal.

Stack

  • Python / Flask
  • Azure AI Search
  • Azure SQL
  • Databricks SQL
  • Azure Functions

More projects

Group customer service, construction chemicals

2026
11Countries on one platform

One contact centre for eleven countries — one platform, not eleven islands

Customer service across eleven countries over voice and SMS, with data from SAP, Salesforce and the product system surfaced directly in the agent workspace. Country differences are configuration, not separate installations.

  • Twilio Flex
  • Twilio TaskRouter
  • Twilio Studio
  • SAP

Group compliance, construction chemicals

2026
10,872Documents generated

10,872 EU declarations of conformity, not copy-paste

New EU packaging regulation required declarations of conformity in 12 languages for around 900 packaging materials. Instead of filling them in one by one, a pipeline generates and files them in a single run.

  • Python
  • docxtpl / Jinja2
  • Microsoft Word COM
  • Microsoft Graph API

Sales & inside sales, construction chemicals

2026
1 + 3Orchestrator and specialist agents

One orchestrator, three specialist agents for product questions

Instead of one agent with three knowledge sources, a parent agent routes to three specialists — one each for ERP article data, product content and SharePoint documents.

  • Microsoft Copilot Studio
  • Azure AI Search
  • Azure Functions
  • SharePoint

A similar process at your company?

30 minutes, free, no sales pitch. You describe what is costing you time — I tell you honestly whether automation is worth it.