Aronlight — Quoting Agent Technical Brief

Aronlight — Adaptto AI Quoting Agent — Technical Brief for Riccardo

Project metadata
StatusResearch complete. All 19 RFQ cases assessed (17 done/partial, 2 pending file resend). Questions for Manel: deployed at https://aronlight-questions.pages.dev (Batch 1 ready, awaiting answers). Odoo staging matched across all cases.
Stack target (build)Django + Vue 3 (Riccardo's standard stack)
Odoo version18
ProtocolXML-RPC at /xmlrpc/2/common + /xmlrpc/2/object
Staginghttps://stage-aronlight.odoo.com / DB: aronlight-staging-32576046
GitHub (research repo)https://github.com/Adapt-to-AI-B2B/aronlight-erp-research
Research byGiuseppe Belpiede (scripts 01–06, email taxonomy, SKU matching) + Raffaello Starace (cases 01-06 deep-dives, skills, Q-bank)

Key terms — expand before reading

Lighting

DALI
Digital Addressable Lighting Interface. A protocol for individually dimming/controlling luminaires. When a project specifies DALI, each luminaire needs a separate DALI-compatible driver (ILDV- SKU) as an additional Odoo line.
IP rating
Ingress Protection (e.g., IP65). Two digits: dust + water resistance. IP20 = indoor only; IP44 = splash-proof; IP65 = outdoor/waterproof. Hard constraint — cannot substitute IP20 for IP65.
IK rating
Impact resistance (IK06–IK10). Required in public/industrial spaces. Lower IK cannot substitute higher IK.
CRI (Ra)
Color Rendering Index (0–100). How accurately the light renders colors. CRI80 = standard commercial; CRI90+ = architectural/premium.
lm (lumens)
Total light output. Primary spec for equivalence matching. Research shows ±20% is acceptable in practice.
ou equivalente
Portuguese for "or equivalent." Standard phrase in specs and public tenders — customer accepts a substitute if it meets the same technical requirements. ~50% of RFQs use this.

Aronlight SKU prefixes

ILAR-XXXXX
Luminaire products — the actual light fixtures. ~3,944 active SKUs follow this format.
ILDV-XXXXX
DALI drivers. Added as a separate Odoo line on DALI projects, selected by wattage range.
ILEM-XXXXX
Emergency units. Added when a spec requires maintained emergency lighting.
PRJP- / PRJE- / ARDV- etc.
Non-standard prefixes on 380 products (8.8% of catalog) — likely bespoke/project items. The matching engine needs explicit handling for these.

Odoo

line_note
A sale.order.line with display_type='line_note'. Zero price, zero qty — used as a section header or label (e.g., "IL01", "alternativa"). Not a product line.
customer_rank
Odoo field intended to flag a partner as a customer. Found unreliable in staging: 0 on many real buyers. Do NOT filter by this field.
sale.order / sale.order.line
Odoo's quote record and its line items. The agent writes here only after human approval.

Document types (PT/ES)

Caderno de encargos
Portuguese "specification booklet." Defines each luminaire type (Tipo A, Lum_A01.x). Often the only source for decoding project-internal codes.
Medición
Spanish BOQ equivalent — numbered list of materials/quantities, typically in Excel.
Orçamento
Portuguese for "quote." Maps to a sale.order in Odoo.

What we need from you

Five decisions before Phase 1 starts:

  1. GitHub — New repo under the Aronlight org (recommended: aronlight-quoting-agent), or build inside this research repo? The research scripts (01–06) live here and can be ported directly to Django.
  2. First task — We propose: Django project setup + catalog sync management command (Phase 1). Does this match how you want to start, or do you have a different entry point?
  3. Stack — Confirm: Django + Vue 3 + PostgreSQL + Redis + Celery + Docker Compose. Any deviations?
  4. Manel call — Do you want to be on the call with Manel when we go through Batch 1 questions, or receive answers async when they come in?
  5. Timeline — When can Phase 1 start? Phase 2 is the first point where Manel's answers become relevant.
  6. Agent vs. platform — We are starting with a single quoting agent for Aronlight. At what point does it make sense to think about this as a platform — one that handles multiple types of agents, potentially across clients? Should the architecture account for that from Phase 1, or do we build lean and revisit?
  7. AI best practices alignment — We will share a Best Practices Guide for AI systems separately (covers the maturity path from single agent to autonomous platform). When should we review it together — before Phase 1 starts, or once the first version is running?

TL;DR — What we're building and why

The problem: Aronlight receives ~3,000 RFQ emails per month. Half list competitor product codes (Philips, iGuzzini, KATOA) with "ou equivalente." Each rep manually looks up competitor specs, finds the closest Aronlight product, and builds the quote in Odoo line by line. A complex RFQ takes 30-60 min. It is the bottleneck.

What the Adaptto AI Quoting Agent does: Takes the input — email, Excel, PDF, or image — extracts the product line items (regardless of how they are described: Aronlight SKU, Aronlight product name, competitor model, or vague description), finds the right Aronlight product for each line, and generates a draft Odoo sale.order ready for the sales rep to review and approve. The rep goes from reading the email to clicking "Create a proposal in Odoo" in under 5 minutes.

What needs to be taken into consideration: Inputs arrive in multiple formats — email body, Excel BOQ, PDF specification, or image. Each needs a different extraction path before we even start matching. Inside those inputs, customers write SKUs inconsistently ("ILAR01691", "ilar 1691", "IL-AR-01691") vs the catalog format ("ILAR-01691"). Many inputs are also BOQs (Bill of Quantities — see Q5), where one customer line expands to 2-3 Odoo order lines. All three — format handling, SKU normalisation, and BOQ expansion — affect matching accuracy before we even reach the spec comparison step.

What's been verified (this repo):

The three hardest problems:

  1. Competitor reference matching (~50% of volume — estimated from 19-case sample) — a subset of customers send a Philips, iGuzzini, or KATOA code and ask for an Aronlight equivalent. Others request Aronlight products directly by SKU or name — the agent handles both. For competitor refs, we need spec comparison. Wattage is not a hard filter (+11% to +140% accepted). Dimensions and IP rating are.
  2. BOQ expansion — 1 customer line becomes 2-3 Odoo lines (luminaire + driver if DALI + accessory if applicable + emergency if required). The system needs to know the expansion rules per product family.
  3. Catalog quality — 1,874 of 4,416 products sit under categ_id="All" with specs buried in free text. This limits automated spec matching until Manel recategorizes them.

What is a BOQ? A Bill of Quantities (BOQ) is the document an architect or contractor sends to suppliers when specifying lighting for a construction project. It lists numbered positions — one per fixture type — with a quantity, a competitor brand+model reference, and sometimes specs. Example: "Position 2 — Philips DN142B, 110 units, round recessed downlight, IP20, 3000K, ou equivalente." The BOQ is not an order; it is a specification. Aronlight's job is to propose equivalent products for each position and quote a price. A single BOQ position typically becomes 2-3 lines in Odoo (luminaire + driver + accessory).


Full flow — email in, Odoo quote out

  Customer email / Excel / image

┌─────────────────────────┐

│ Parse input │ Extract line items + detect format

│ (Claude API) │ (email / Excel / PDF / image)

│ │ Attachment-only → request file

└─────────────────────────┘

┌─────────────────────────┐

│ Match client │ Email exact → VAT → fuzzy name (WRatio)

│ │ ≥75% auto · <75% ⚠ human confirms

└─────────────────────────┘

┌─────────────────────────┐

│ Match each line item │ Exact SKU → alias table → fuzzy name

│ │ → spec match on catalog text

│ │ ≥90% auto · 60-89% ⚠ review · <60% ✗

└─────────────────────────┘

┌─────────────────────────┐

│ BOQ expansion │ 1 input line → N Odoo lines

│ │ luminaire + driver (DALI?) + accessory

│ │ + emergency (ILEM-?) per product family

└─────────────────────────┘

┌─────────────────────────┐

│ Human review │ Sales rep sees flagged items, confirms

│ (5 gates) │ or corrects. Nothing writes to Odoo yet.

└─────────────────────────┘

┌─────────────────────────┐

│ Odoo write │ draft sale.order + sale.order.line

│ (on approval only) │ position labels as line_note

└─────────────────────────┘

Sales rep sends

quote to customer

Input types handled:

TypeDescriptionVolumeClaude needed?
ACustomer uses Aronlight SKU directly~5%No — regex
BCustomer uses Aronlight product name, no SKU~15%Yes — extraction
CCompetitor reference + specs, "ou equivalente"~50%Yes — spec match
DConversational / advisory request in prose~10%Yes — full NLP
EAttachment only, nothing in email body~32%No — escalate

Q1 — Catalog access: API verified, specs live in product name not structured fields

Question: Can we connect and get the full product catalog?

Answer: YES — clean data, ready for production.

Findings (01_catalog_access.py on staging):

CRITICAL CATALOG GAP (from Raffaello's case analysis):

⚠️ Verified 2026-05-31 — structured spec fields exist but are 0% populated

The schema is well-designed; it is simply not filled in. Across 4,416 active products:

FieldProducts with a value
ip_rating2 (0.0%)
dali0 (0.0%)
rated_power_w / max_led_power1 / 0
led_colour_temperature_k1
dimensions / width_mm / height_mm1 / 0 / 0
colour_rendering_index_cri1

Decisive contrast: the dali field is populated on 0 products, yet 229 active products mention "dali" in the name. The data exists — only in free text.

Design impact: the matching engine must parse specs from the product name (regex + LLM); it cannot rely on structured attributes. This is more fragile (depends on naming consistency across PT/ES plus abbreviations) and is the single most important constraint for the Q4 SKU-matching design.

Latent opportunity: if Aronlight ever populates these fields (manually, or via a one-time name→field extraction), matching upgrades from "parse fragile prose" to "exact field filter" — high-value, but not available today. Build assuming free text. (Full detail: answered-questions.md, Q-002 / Q-033.)

Staging reliability warning:

XML-RPC auth pattern (reuse in Django):

import xmlrpc.client

common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")

uid = common.authenticate(DB, USER, PASSWORD, {})

models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")

uid is stable within a session — cache it

Multi-company scope:

Django model recommendation:

class OdooProduct(Model):

odoo_id = IntField(unique=True)

sku = CharField(max_length=50, unique=True) # ILAR-XXXXX or ILDV-XXXXX

name = TextField()

odoo_category = CharField(max_length=200)

norm_category = CharField(max_length=50) # see Q1b

list_price_eur = DecimalField()

company_id = IntField() # 2=ES, 3=PT

raw_description = TextField() # for spec text-parsing

synced_at = DateTimeField()

Gaps:


Q1b — Categorization: 18-category schema covers 89%, ~43% of products need manual review

Question: How do we create a normalized category schema for matching?

Answer: YES — 18-category EN schema covers 89% of parseable catalog. Real gap is ~43%.

What the schema is: Aronlight's Odoo uses Portuguese category names ("Tiras LED", "Painéis LED", "Lâmpadas"). The 18-category schema is a static mapping table: Odoo category name → EN key, with keyword fallback on the product name. This translation runs at sync time so Django always stores a consistent English key regardless of Odoo's internal naming.

Findings (02_categorization.py on staging):

18 Normalized Categories (EN):

CategoryProductsDescription
BULBS1,098A60, G45, GU10, G9, G4, filament, flame
LED_STRIPS702Strip lights, neon flex, RGB
DOWNLIGHTS583Recessed spots, commercial ceiling
OUTDOOR_SPOTLIGHTS342Floodlights, IP65+ projectors
ACCESSORIES318Connectors, profiles, optics
DRIVERS_POWER248Drivers, power supplies, DALI
SMART_CONTROL207WiFi/Zigbee/RF controllers, MiBoxer
PANELS175LED flat panels, slim panels
TUBES118T8/T5 LED tubes
INDUSTRIAL86High-bay, ATEX, linear armatures
SENSORS_DETECTORS56PIR, microwave motion sensors
EMERGENCY31Emergency lighting, exit signs
DECORATIVE28Pendants, domestic decorative
OUTDOOR_WALL19Exterior wall lights
SOLAR5Solar-powered fixtures
HORTICULTURE5Grow lights
STREET_LIGHTING4Street / public lighting
TRACKS_RAILS5Track systems + accessories
UNCATEGORIZED494+Needs manual review (real number likely higher)

Recommendation for Riccardo: store norm_category on the cached product. Run the mapping at sync time, not at query time. Ship without the "All" category products first — ask Manel to recategorize them in Odoo as a precondition for full coverage.


Q2 — Client matching: 100% on test cases, drop customer_rank filter

Question: Can we match a client name from email/Excel to a partner in Odoo?

Answer: YES — 100% match rate on 7 real PT/ES company name variants. Key fix: drop customer_rank filter.

Findings (03_client_matching.py on staging):

CRITICAL FIX from Raffaello (match-orcamento skill):

Match strategy (working code in 03_client_matching.py):

  1. Exact email match against res.partner.email (most reliable — use first)
  2. Exact VAT/NIF/NIPC match against res.partner.vat (instant, skip fuzzy)
  3. Strip legal suffixes: LDA, SRL, S.L., UNIPESSOAL, LTDA, GmbH, LLC, etc.
  4. Normalize: lowercase, remove special chars, collapse whitespace
  5. Scorer 1: rapidfuzz.fuzz.WRatio on normalized names (primary)
  6. Scorer 2: rapidfuzz.fuzz.partial_ratio on original names (fallback)
  7. Threshold: ≥75% → match, <75% → flag as new client

Match tiers for the UI:

Quote matching (from Raffaello's /match-orcamento skill):

After client match, rank existing sale.order records by quantity signature overlap to identify the "live version" (latest revision) vs. confirmed sales (state=sale). Useful for detecting if an RFQ is a revision of an existing quote.

Django model:

class OdooPartner(Model):

odoo_id = IntField(unique=True)

name = CharField(max_length=200)

vat = CharField(max_length=50, null=True)

email = EmailField(null=True)

company_id = IntField() # 2=ES, 3=PT

synced_at = DateTimeField()


Q3 — Input parsing: 5 types defined and tested on 19 real RFQ emails

Question: Can we extract SKUs + quantities from all 3 input types?

Status: COMPLETE — 19 real RFQ emails collected, parsed, and matched. All 5 scripts validated.

Real input type taxonomy (from 19 Aronlight RFQ emails + 6 Raffaello case studies — PT/ES market):

Type A — Direct SKU request (~5% of volume)

Example: Solicitud saltoki (Saltoki, 2026-03-05)

Type B — Aronlight product name, no SKU (~15% of volume)

Examples: Precios Excelsior, Pedido de proposta Carolina Arquitecta (BERG/CUBE/OTTO/LUPO names)

Type C — Competitor reference substitution (~50% of volume — estimated from 19-case sample)

*Examples: Philips (AlpLuz, Alternativas), Aura Light (Pedido URGENTE), Karizmaluce/Castaldi (Centro Náutico),

KATOA/CLIMAR (Centro Saúde Chaves), Performance in Lighting (Editora Barcelos), Iguzzini (San Juan DALI),

Philips BVP527 (Proyectores Campo Futbol), Duralamp (Biobanco)*

Type D — Conversational / technical advisory (~10% of volume)

Example: Ajuda técnica (Manuel & Sónia, 2026-05-07)

Type E — Attachment-only / BOQ with internal project codes (~20% of volume)

*Examples: Hospital Pediátrico Saurimo ("ver anexos"), Proposta LUMIS DST SA, REQ IP Bragança,

APPACDM Sabrosa ("mapa em anexo"), Metro Madrid (LDV quote), RESIDENCIA DALI (DWG files),

Biobanco Azul (Lum_A01.x codes), Edifício Editora Barcelos (Performance in Lighting)*

Parser validation results (04_input_parser.py against real emails):

TypeEmailItems extractedUnclear flags
ASaltoki (ILAR- SKUs)42 (ambiguous controller choice, accessory qty)
CAlpLuz (Philips refs)93 (accessory quantities inferred)
DAjuda técnica41 (ILAR-01993 used twice, role unclear)
EHospital Saurimo11 (entire list in attachment)

Aronlight-specific signal patterns:

SKU pattern:       /ILAR-\d{5}/  or  /ILDV-\d{5}/

Quantity patterns: "(\d+)\s*(un|uni|uds|uds\.|m|ml|mts|metros)"

Competitor brands: Philips, Osram, Aura Light, Karizmaluce, Castaldi, Encapsulite, Borealis,

Schuch, Iguzzini, KATOA, Soneres, Lucens, Schréder, LDV

Spec signals: W (wattage), K (color temp), lm (lumens), IP (protection), IK (impact),

mm/cm (dimensions), deg (beam angle), CRI/Ra (color rendering)


Q4 — SKU matching: exact and fuzzy work, competitor refs are the remaining gap

Question: How do we match input SKU (customer's code) to Aronlight's Odoo SKU?

Answer: PARTIAL — exact/fuzzy SKU works, competitor-ref-to-Aronlight-SKU requires a new layer.

Validation results (05_sku_matching.py against real parsed emails):

Input typeItemsExact matchFuzzy matchNo match
Type A (ILAR- direct)43 (100%)01 (ambiguous)
Type C (Philips refs)9009 (100%)
Type B (Aronlight names)1001 (64% — BERG→Trimless)9

Match tiers:

Match register (data/sku_match_register.json):

Every match is logged. Over time, customer SKU → Aronlight SKU mappings accumulate.

This register becomes the translation layer — no re-matching needed for repeat customers.

Critical gap — Type C (50% of all requests):

Competitor refs (Philips RS150B, Iguzzini, KATOA) return 0 matches because script 05 only

does name/SKU string matching. The Philips specs ARE in the extracted items

(IP65, D78mm, 7.2W, 3000K) but there's no spec-field comparison against the Aronlight catalog.

Required for production — three additional components:

  1. Spec matching layer — compare extracted specs against catalog product attributes.
  1. Product name alias table — Aronlight short names (BERG, CUBE, OTTO, PRADA, LUPO, NEXOR)
  1. Unknown brand lookup table — competitor brand "OH" blocked 54% of units in one real case.

Q5 — BOQ expansion: one customer line becomes 2 to 3 Odoo lines

Question: Does 1 BOQ line = 1 Odoo order line?

Answer: NO. Verified with real Aronlight quote PDFs from Odoo staging.

A customer sends a BOQ with numbered positions — e.g., 7 positions (one per product type). When Raffaello pulled the actual Aronlight quote for that RFQ from Odoo, it had 17 lines. But 8 of those 17 are display_type section headers (no product, no price) — not real order lines. Real product lines: 9. Expansion: ~1.3× per position, not 2.4× as previously stated. The 2.4× figure was an artefact of counting section headers as product lines.

Each position expands depending on what the product needs:

Basic — luminaire only (no accessories, no DALI):

[line_note]    "10.4.5.1.1   IL01"     ← position label, price=0, qty=0

[product] ILAR-01108 ← luminaire

With a decorative accessory (e.g., wall light + ring):

[line_note]    "10.4.5.1.5   IL05"

[product] ILAR-02971 ← luminaire (RUMU)

[product] ILAR-02976 ← decorative ring, same qty as luminaire

With two alternatives (specs don't match exactly):

[line_note]    "10.4.5.1.4   IL04"

[product] ILAR-00960 ← main option (Fisher IP54)

[line_note] "alternativa"

[product] ILAR-02683 ← alternative (Noa IP65)

DALI project — adds a driver line per position:

[line_note]    position code

[product] ILAR-XXXXX ← luminaire

[product] ILDV-XXXXX ← driver (SKU selected by wattage range, reused across positions)

[product] ILEM-XXXXX ← emergency unit if required (distinct prefix)

What this means for the data model:

Product dependency table — design requirement for Riccardo:

Some products are not sold standalone. When the system proposes product X, it must also add product Y. Three types observed:

TypeTriggerCompanion productExample
Mandatory accessoryAlwaysDecorative ring, bezelRUMU wall light → ring
DALI driverIf project specifies DALIILDV- driver (selected by wattage range)Any ILAR- luminaire on a DALI project
Emergency unitIf project requires maintained emergencyILEM- unitAny luminaire in a public/regulated space

Critical: these rules are not in Odoo today. The accessory_product_ids field in product.template exists but is almost empty (2 of 4,324 products populated). The expansion rules live in the team's tacit knowledge, not in any structured data source. Manel must define them before the system can generate correct Odoo quotes.

Design implication: the system needs a product dependency table in the database — configurable by Manel or the sales team, not hardcoded. Structure: (product_sku, companion_sku, dependency_type, trigger_condition). Without it, the system can match the right luminaire but will generate an incomplete quote that a sales rep must manually correct every time.

Unit normalization gap:


Q6 — Wattage tolerance: last priority in spec matching, not a hard filter

Key insight: wattage is NOT a hard filter for competitor substitution.

Commercial staff accept wide tolerance: iGuzzini case showed +108% wattage deviation accepted; Philips cases ranged from +11% to +140%. The actual matching priority for competitor substitution is:

  1. Category + mounting method (must match: recessed ≠ surface ≠ pendant)
  2. Cutting dimensions (most critical for recessed: a 68mm hole won't fit a 78mm fitting)
  3. IP rating (IP65 outdoors cannot be replaced with IP20)
  4. Luminous flux / lm (approximate — ±20% acceptable)
  5. Color temperature K (2700K vs 3000K is a design choice, not hard constraint)
  6. Wattage (last priority — guide only, not a filter)
  7. CRI, IK, driver type, diffuser, finish (secondary — flag if different, don't block)

Implication for spec matching: filter first by category + dimensions + IP, then rank by lm proximity. Never reject a candidate solely for wattage mismatch.


Q7 — Output states: no-match is a valid result, always surface it explicitly

"No equivalent" is a valid system output.

Non-automatable cases detected in real RFQs:

System must detect these and route to human with explicit "no equivalent found" status — not leave them as unclear.

Alternative proposals pattern (from Raffaello's case 02):

When specs don't match exactly, commercial staff propose:

System should generate paired candidates (primary + fallback), not force a single match.

RfqLineItem status values (revised):

STATUS_CHOICES = [

("auto", "Auto-matched — no review needed"),

("review", "Proposed match — sales rep confirms"),

("unclear", "Needs customer clarification"),

("no_match", "No Aronlight equivalent found"),

("non_automatable", "Requires human expert + photometric docs"),

]


Architecture recommendation for Riccardo

Django models needed:

OdooProduct      # cached catalog (sync daily via management command)

OdooPartner # cached customers (sync daily)

ProductAlias # short name → OdooProduct (BERG, CUBE, OTTO, PRADA, LUPO, NEXOR)

BrandLookup # competitor brand prefix → canonical brand name (fix "OH" problem)

SkuMapping # customer SKU / product name → Aronlight SKU register (cumulative)

RfqRequest # inbound request: source, raw content, parsed items, status

RfqLineItem # parsed line + match result + status (auto/review/unclear/no_match/non_automatable)

RfqOdooLine # expanded Odoo line per RfqLineItem (1 RFQ line → N Odoo lines)

Odoo API call pattern:

Input pipeline:

Email / Excel / Image

Classify input type (A/B/C/D/E/BOQ) — per-line, not per-email

04_input_parser.py logic (Claude API for Type B/C/D)

Structured items: [{description, qty, unit, sku_hint, specs, confidence}]

Unit normalization (m→rolls, etc.)

05_sku_matching.py logic (email → VAT → fuzzy name → spec match)

BOQ expansion: 1 line → [luminaire, driver, accessories, emergency]

RfqLineItem with status: auto | review | unclear | no_match | non_automatable

Human gate → Odoo draft sale.order

Human-in-the-loop gates (5 mandatory):

  1. Client match < 75% → stop, show "new client?" prompt
  2. Any unclear item → stop, show clarification question for customer
  3. SKU match 60–89% → show proposed match, require sales rep confirm
  4. no_match or non_automatable item → stop, route to senior sales
  5. Final order → always human approval before Odoo write

Claude API usage in production:

Pre-built skills from Raffaello (reuse these):


What the product looks like end to end

A sales rep gets an RFQ email. Here's what they do in the agent from start to finish:

Screen 1 — Input

┌─────────────────────────────────────────────────────────────┐

│ Adaptto AI Quoting Agent Aronlight PT │

├─────────────────────────────────────────────────────────────┤

│ │

│ [Paste email text] [Upload Excel] [Upload image/PDF] │

│ │

│ ┌───────────────────────────────────────────────────────┐ │

│ │ Subject: Pedido cotação — AlpLuz — Philips refs │ │

│ │ From: alpluz.lda@gmail.com │ │

│ │ ... │ │

│ └───────────────────────────────────────────────────────┘ │

│ │

│ [Parse →] │

└─────────────────────────────────────────────────────────────┘

Screen 2 — Review: client + line items

┌─────────────────────────────────────────────────────────────┐

│ Adaptto AI Quoting Agent Aronlight PT │

├──────────────────────────────────┬──────────────────────────┤

│ CLIENT │ 7 items · 2 need review │

│ AlpLuz Lda ✓ 98% match │ │

│ partner_id 148126 │ [Create in Odoo ▸] │

├──────────────────────────────────┴──────────────────────────┤

│ # Input Match Status │

│ ──────────────────────────────────────────────────────── ─ │

│ 1 RS150B · 33 un · IP65 3000K Luso 8W ✓ auto │

│ 2 DN142B · 110 un · IP20 3000K Una II 15W ⚠ review│

│ 3 RC132V · 22 un · 300×1200mm Painel Backlit ✓ auto │

│ 4 WL055V · 22 un · wall IP65 Fisher IP54 ⚠ review│

│ + Noa IP65 alt │

│ 5 WL140V · 22 un · circular RUMU + Aro Preto ✓ auto│

│ 6 WL140V · 11 un · no ring RUMU ✓ auto │

│ 7 WT120C · 11 un · 600mm IP65 TITAN 1200 ✓ auto │

└─────────────────────────────────────────────────────────────┘

Screen 3 — Review a flagged item

┌─────────────────────────────────────────────────────────────┐

│ ← Review item 1 of 2 │

├─────────────────────────────────────────────────────────────┤

│ Customer wrote: "DN142B 110 un" │

│ Competitor: Philips DN142B · 9.8W · IP20 · 3000K │

│ round downlight · recessed │

│ │

│ Proposed match: Una II ILAR-01571 │

│ 15W (+53%) · IP65/44 · 3CCT · €29.90/un │

│ Match score: 78% │

│ ⚠ Wattage over-spec · IP upgraded │

│ │

│ [✓ Accept] [Search catalog] [Mark unclear] [No match] │

└─────────────────────────────────────────────────────────────┘

Screen 4 — Odoo preview before write

┌─────────────────────────────────────────────────────────────┐

│ Odoo Preview — AlpLuz Lda draft │

├─────────────────────────────────────────────────────────────┤

│ [note] IL01 │

│ ILAR-01108 Luso 8W D80 IP65 ×33 € 962.61 │

│ [note] IL02 │

│ ILAR-01571 Una II 15W 3CCT IP65/44 ×110 €3,289.00 │

│ [note] IL03 │

│ ILAR-02979 Painel 300×1200 Backlit ×22 € 781.00 │

│ [note] IL04 │

│ ILAR-00960 Fisher 10W IP54 ×22 €1,031.36 │

│ [note] alternativa │

│ ILAR-02683 Noa 12W 3CCT IP65 ×22 € 658.68 │

│ [note] IL05 │

│ ILAR-02971 RUMU 9-16W Switch ×22 € 517.00 │

│ ILAR-02976 Aro Bezel Preto ×22 € 46.64 │

│ [note] IL06 │

│ ILAR-02971 RUMU 9-16W Switch ×11 € 258.50 │

│ [note] IL07 │

│ ILAR-02135 TITAN 1200 IP65 36W ×11 € 471.35 │

│ ─────────────────────────────────────────────────────── │

│ Total €4,732.73 │

│ │

│ [✓ Create in Odoo] [Export PDF] [← Back to review] │

└─────────────────────────────────────────────────────────────┘

The Odoo preview is the real quote structure — position labels as line_note, accessories on their own line, alternatives separated by an "alternativa" note. Exactly what Aronlight's sales team produces manually today.


Build constraints — non-negotiables from the research

Findings that must be respected regardless of implementation choices:

  1. Do not filter partners by customer_rank — 36% of real buyers have customer_rank=0 in staging. Use email exact match → VAT exact match → fuzzy name as the chain instead.
  2. Spec matching must parse product name free text — the structured spec fields (ip_rating, dali, rated_power_w, etc.) exist in the Odoo schema but are 0% populated across all 4,416 products. There is no field-query shortcut.
  3. Query both companiescompany_id=2 (ES) and company_id=3 (PT). Partners are shared; sales reps sometimes create quotes in the wrong entity. Tag origin on every cached record, never filter by company during matching.
  4. Do not build Phase 4 until Manel answers Q 1-8, 1-9, 1-10 — BOQ expansion rules (what drives 1→N lines, accessory bundling, driver line format) are unconfirmed. Building the wrong structure means every Odoo write produces the wrong quote.
  5. Human approval before every Odoo write — no auto-create of sale.order. The agent proposes; the sales rep approves. Hard gate, not a preference.
  6. ILAR- / ILDV- / ILEM- are distinct product families — never mix them in expansion logic. ILAR = luminaire, ILDV = driver (DALI projects only), ILEM = emergency unit.
  7. Wattage is not a hard filter for competitor substitution — research confirmed +11% to +140% deviation accepted in real quotes. Filter first by category + dimensions + IP, rank by lm proximity. Rejecting on wattage alone produces wrong matches.
  8. Product dependency rules do not exist in Odoo — they must be built — the accessory_product_ids field is almost empty (2 of 4,324 products). The system needs a configurable product dependency table (product_sku → companion_sku + type + trigger). Without it, the system can match the right luminaire but cannot generate a complete Odoo quote. Manel must supply the rules; the data model must exist before Phase 4.

How to build it — phased plan

Research is done. Here's the recommended build sequence, scoped to what's verified and unblocked.

What you can build now vs. what must wait for Manel

PhaseCan start immediatelyMust wait for Manel answer
Phase 1 — catalog syncYes — Django + Odoo connection fully verifiedNothing blocking Phase 1. Note: 1,874 products sit under categ_id="All" with no useful signal — spec matching in Phase 3 will have a ~43% blindspot until these are recategorized in Odoo.
Phase 2 — parsing + client matchingYes — all 5 input types defined, client match logic tested on 19 real casesNothing blocking.
Phase 3 — SKU matchingType A (exact SKU) and Type B (Aronlight name) can shipQ 1-2 (equivalence thresholds — IP, lm, wattage tolerance), Q 1-6 (IP upgrade acceptable?), Q 2-1 (DALI default), Q 2-2 (dimension vs wattage priority). Without these, Type C (50% of volume) matching rules are guesses. Note: spec field format is confirmed — fields exist in schema but are 0% populated; matching must parse product name free text.
Phase 4 — BOQ expansion + Odoo writeNothing — do not build until Manel answers Q 1-8, 1-9, 1-10Q 1-8 (what drives 1→N expansion), Q 1-9 (accessory as separate line or bundle), Q 1-10 (driver always separate line?). Getting these wrong means every Odoo write produces the wrong structure.

Phase 1 — Catalog sync (weeks 1–2)

Goal: Django talks to Odoo, local DB is populated, sync runs daily.

Phase 2 — Input parsing + client matching (weeks 3–4)

Goal: sales rep pastes an email, gets a client match and a list of extracted items.

Phase 3 — SKU matching + review UI (weeks 5–7)

Goal: each line item gets a proposed Aronlight match; sales rep can accept, reject, or reassign.

Phase 4 — BOQ expansion + Odoo write (weeks 8–10)

Goal: approved matches become a real Odoo draft sale.order.

Open scoping decisions — to discuss with team

These three are NOT decided. They need a call with Manel and Riccardo before Phase 1 starts.

DecisionOption AOption BWhat it affects
Image/PDF parsingSkip in MVP — email + Excel onlyInclude from day one20% of volume (Type E). A = faster ship, reps still handle image RFQs manually
Multi-company PT/ESStart PT only, add ES laterBoth from day oneCase 06 is ES. A = simpler build; B = only ~1 day extra if sync already tags company_id
Type C spec matchingShip as "review" — manual assignmentFull auto spec match in v150% of all volume. A = faster ship but half the value proposition is still manual

How to use the training data for spec matching

The case studies produced 47 competitor product datasheets across 10 cases in docs/email_samples/*/research/*.md. Each one is a structured record: competitor brand + model, specs (dimensions, W, lm, K, CRI, IP, IK, driver type, mounting), and — for cases 02, 04, 06 — the correct Aronlight match is in match_summary.md.

This gives us labeled pairs: (competitor specs) → (Aronlight SKU + reasoning).

Three ways to use them:

1. Test dataset — measure accuracy before shipping

Parse all 47 datasheets into a JSON:

[

{

"competitor_ref": "Philips RS150B",

"specs": {"dimensions_mm": 78, "W": 7.2, "IP": 65, "K": 3000, "mounting": "recessed", "shape": "round"},

"correct_aronlight_sku": "ILAR-01108",

"correct_name": "Luso 8W D80 IP65"

},

...

]

Run the spec matching algorithm against all 43 before shipping. Target: ≥80% correct SKU without human intervention. Anything below is a signal to tune the scoring weights, not to ship.

2. Few-shot examples for Claude at runtime (Type C)

When a new competitor ref arrives, send 2–3 similar examples as context in the Claude prompt:

Match competitor lighting products to Aronlight catalog items.

Example 1:

Input: Philips RS150B · 7.2W · IP65 · ø78mm · 3000K · recessed spot

Match: ILAR-01108 Luso 8W D80 IP65 · reason: ø78≈80mm, IP65, same category

Example 2:

Input: KATOA BOX-E · 42W · IP20 · 597×597mm · 4000K · recessed panel

Match: ILAR-XXXXX [Aronlight panel 600×600] · reason: panel format, dimensions match

Now match: [new competitor ref]

The examples anchor Claude to Aronlight's actual substitution logic (wattage tolerance, IP upgrade acceptable, dimensions primary). Without them, Claude defaults to generic LED knowledge which doesn't match how Aronlight commercial staff actually quote.

3. Category-specific scoring weights

The 43 cases reveal that the right spec fields vary by product category — not one universal scoring function:

CategoryHard filters (must match)Ranking fieldsIgnored
DOWNLIGHTScutting dimension ±5mm, mounting typelm ±20%, Kwattage
OUTDOOR (IP65+)IP rating (exact or higher), categorydimensions, lmwattage
PANELSpanel size (600×600 vs 300×1200)K, lmwattage
DALI projectsDALI flag (hard)lm, Kwattage, finish
LED_STRIPSIP, KW/mtotal lumens

Build the spec matcher as a category-aware scorer, not a flat field comparison. The category is always known from norm_category on OdooProduct.


Phase 3 — Match-Orcamento findings (all 13 cases)

All 13 RFQ cases were matched against Odoo staging using rapidfuzz + email/VAT/fuzzy-name strategy. Key findings for the build:

Odoo data quality issues (flag for Riccardo)

1. Email-to-partner mismatch (case 10 — Carolina Arquitecta)

carolinaportugalarq@gmail.com is assigned in Odoo to "Robin Kathuria" (VAT 312288743), not to Carolina Portugal. The email match returns the wrong partner with 100% confidence. The system would silently create a quote under the wrong customer. Action needed: data cleanup in Odoo + detect mismatch when VAT and email owner diverge.

2. VAT duplicate (case 11 — DELPA / Esfera de Fios)

Two different partners share VAT 514865881: DELPA ELECTRICPARTS LDA and Esfera de Fios. VAT is supposed to be a unique identifier in Odoo. Deduplication needed before VAT matching can be trusted for PT companies.

3. @aronlight.es email as CC (case 17 — Jordi Marco)

Every ES customer record in Odoo includes an @aronlight.es address as CC. When the sender is jordi.marco@aronlight.es, domain matching on aronlight.es returns the entire ES distributor network (40+ partners all score 100%). This breaks partner disambiguation for internally-forwarded requests. Action needed: detect @aronlight.es sender as internal channel (Q 2-16); route differently than external client emails.

Match results summary

CaseClient foundOrdersBest overlapVerdict
07 Biobanco AzulL3W (domain)4038%CANDIDATO DÉBIL
08 Editora BarcelosARMASUL SA (domain)25333%PEDIDO NO LOCALIZADO
09 Hospital SaurimoRABISCOS ORDENADOS (exact)0N/A (E2)SEM PEDIDOS — BOQ parsed: 2,527 units, 28 types, no brand refs. Critical gap: 658 ASEPTIC panels (A3 + A10R DALI for OR). Largest project in dataset.
10 Carolina ArquitectaEmail mismatch → no clientNÃO ENCONTRADO
11 LUMIS DSTDELPA (exact + VAT)43N/A (E2)ENCONTRADO — BOQ parsed: 586× Hoff Light LIBERTAD_6 (6W IP44 recessed round Ø68mm cutout, CCT-switchable). LUMIS = project name, not brand.
12 Metro MadridFrancisco Soler (exact)1 draft (0 lines)N/A1 PEDIDO DRAFT
13 Ajuda técnicaMANUEL & SÓNIA (exact)236100% (degenerate)OVERLAP DEGENERADO
14 Centro NáuticoRODEL (domain)14527%CANDIDATO DÉBIL
15 Emergências ATEXA.R.COSTA (exact)13671%ORÇAMENTO PROBABLE
16 Pedido urgenteSTRADA (domain)18825%PEDIDO NO LOCALIZADO
17 Precios ExcelsiorAronlight internal CC flood40+CANAL INTERNO
18 Proyectores futbolSURELECTRIC (exact)1unrelatedPEDIDO NO LOCALIZADO
19 Solicitud Saltokisaltoki.es = 40+ branches6PRECISA VAT

Key architectural implications

Degenerate overlap (case 13 — Manuel & Sónia):

OR 2026/9807 is an 85-line blanket order. The [1,2,2] quantity signature of the advisory request matches it at 100% — but it is meaningless because small-quantity signatures match almost any large order. Type D advisory cases need a different matching strategy: don't use qty signature at all, match by recency + client relationship instead.

NON-AUTO type finds active client (case 15 — A.R.Costa ATEX):

A.R.Costa is an active, high-value Aronlight client (136 orders, 71% overlap on OR 2026/11485). The system correctly identifies them even though the product request is out-of-scope (ATEX emergency lighting, brand Cronus, Schuch). The right behavior: always do client matching; flag the line items as NON-AUTO but don't drop the client identity.

Saltoki multi-entity (case 19 — ES distributor with 40+ branches):

Large ES distributors (Saltoki, Rexel, Sonepar) have one branch per city in Odoo. A contact-level email (mzambrano@saltoki.es, individual) doesn't appear in Odoo at all. Domain match returns all branches. Resolution requires VAT-level disambiguation or branch-specific email on the RFQ.

Staging recency gap:

Cases 16 and 18 were sent 2026-05-28 — the same day as the match run. Quotes in progress (Flávio / Ricardo working on them) are not yet in staging Odoo. The system will correctly return "no match" for live in-progress quotes. This is expected, not a bug.


Questions for Manel

All open business policy questions are collected and deployed at:

https://aronlight-questions.pages.dev

The document is organized in three batches:

Key Phase 3 blockers: Q 1-2 (equivalence thresholds), Q 1-6 (IP upgrade tolerance), Q 2-1 (DALI default), Q 2-2 (dimension vs wattage priority).

Key Phase 4 blockers: Q 1-8, 1-9, 1-10 (BOQ expansion rules — now in Batch 1).

Odoo data issues to fix before go-live (for Riccardo, not Manel):

Confirmed: