Skip to content

edSpetchAkn/akeneo-pricing-matrix

Repository files navigation

Pricing Matrix Panel — Akeneo UI Extension

A pim.product.panel UI Extension for Akeneo Serenity SaaS that aggregates multiple pim_catalog_price_collection attributes into a single consolidated table on the Product Edit Form. Instead of opening each price attribute individually, users see all subsidiary prices side-by-side in one native-feeling panel.


How it works

  1. When a user opens a product, the extension reads the product UUID from globalThis.PIM.context.product.uuid.
  2. It fetches the full product record via the Akeneo API (PIM.api.product_uuid_v1.get). The SDK handles authentication automatically.
  3. For each configured subsidiary, it looks up the matching attribute in product.values and extracts the price entries (amount + currency).
  4. Currency columns are derived dynamically from the data — no hardcoding required. EUR is always shown first; remaining currencies are sorted alphabetically.
  5. The table renders using Akeneo Design System components for a native look and feel.

Attribute requirements: Each subsidiary attribute must be of type pim_catalog_price_collection in Akeneo and must be assigned to the product's family for values to appear.


File structure

pricing-matrix-panel/
├── src/
│   ├── main.tsx                   # Entry point — mounts React + ThemeProvider
│   ├── App.tsx                    # Root component — wires hook to PricingMatrix
│   ├── config.ts                  # Hardcoded fallback: SUBSIDIARY_PRICE_ATTRIBUTES
│   ├── styled.d.ts                # styled-components v6 theme type augmentation
│   ├── hooks/
│   │   └── usePricingMatrix.ts    # Data-fetching hook
│   └── components/
│       └── PricingMatrix.tsx      # Presentational table component
├── extension_configuration.json   # Akeneo manifest (position, name, label)
├── package.json
├── vite.config.ts                 # Vite build config (reads filename from manifest)
└── tsconfig.json

Configuration

Subsidiary-to-attribute mappings can be managed in two ways. The custom variable approach is strongly preferred because changes take effect immediately in the PIM UI without any rebuild or redeployment.

Option A — Custom variable (recommended)

In Akeneo: Settings → UI Extensions → Pricing Matrix → Custom Variables

Set the variable subsidiary_price_attributes using this format:

Label:attribute_code,Label:attribute_code,...

Example:

AZ:price_az,BI:price_bi,DP:price_dp,EG:price_eg,HC:price_hc,KU:price_ku,MH:price_mh,MI:price_mi

Rules:

  • Each pair is Label:attribute_code separated by commas.
  • Label is the display name shown in the table row (can be any string, no colons).
  • attribute_code must match an existing Akeneo attribute of type pim_catalog_price_collection.
  • Whitespace around commas and colons is ignored.
  • If the variable is empty or absent, the extension falls back to Option B.

Important — manifest declaration requirement. For the SDK to surface a custom variable via PIM.custom_variables, it must be declared as a top-level custom_variables key in the uploaded manifest (not just inside configuration). The extension_configuration.json already includes this declaration:

"custom_variables": {
  "subsidiary_price_attributes": ""
}

When deploying via the API, pass the variable as a top-level form field rather than inside configuration:

-F "custom_variables[subsidiary_price_attributes]=Label:code,..."

Setting the variable only via a configuration.custom_variables JSON PATCH is not sufficient — the SDK will not read it.

Option B — Hardcoded fallback

Edit src/config.ts and update the SUBSIDIARY_PRICE_ATTRIBUTES array:

export const SUBSIDIARY_PRICE_ATTRIBUTES: SubsidiaryConfig[] = [
  { label: 'AZ', attributeCode: 'price_az' },
  { label: 'BI', attributeCode: 'price_bi' },
  // Add or remove entries here...
];

This file is the only file that needs to change when adding or removing subsidiaries. After editing it, a rebuild and redeployment is required.


Building

npm install
npm run build

The compiled bundle is written to dist/pricing_matrix.js.

The output filename is read directly from extension_configuration.json (file field), so vite.config.ts and the manifest are always in sync.


Deploying to Akeneo

The multipart field format for the POST/PATCH differs slightly between Akeneo instance versions. Try Format A first; if it returns "The following fields are not allowed", use Format B.

First-time deployment (create)

Format A — config as a JSON file attachment (most instances):

curl -X POST \
  "https://<your-instance>.cloud.akeneo.com/api/rest/v1/ui-extensions" \
  -H "Authorization: Bearer <token>" \
  -F "extension_configuration=@extension_configuration.json;type=application/json" \
  -F "file=@dist/pricing_matrix.js;type=application/javascript"

Format B — config as individual form fields (some newer/demo instances):

curl -X POST \
  "https://<your-instance>.cloud.akeneo.com/api/rest/v1/ui-extensions" \
  -H "Authorization: Bearer <token>" \
  -F "name=sdk_script_extension_pricing_matrix" \
  -F "type=sdk_script" \
  -F "position=pim.product.panel" \
  -F "configuration[default_label]=Pricing Matrix" \
  -F "configuration[labels][en_US]=Pricing Matrix" \
  -F "file=@dist/pricing_matrix.js;type=application/javascript"

Note the uuid returned in the response — you will need it for future updates.

Updating an existing deployment (PATCH)

Format A:

curl -X PATCH \
  "https://<your-instance>.cloud.akeneo.com/api/rest/v1/ui-extensions/<uuid>" \
  -H "Authorization: Bearer <token>" \
  -F "extension_configuration=@extension_configuration.json;type=application/json" \
  -F "file=@dist/pricing_matrix.js;type=application/javascript"

Format B:

curl -X PATCH \
  "https://<your-instance>.cloud.akeneo.com/api/rest/v1/ui-extensions/<uuid>" \
  -H "Authorization: Bearer <token>" \
  -F "name=sdk_script_extension_pricing_matrix" \
  -F "type=sdk_script" \
  -F "position=pim.product.panel" \
  -F "configuration[default_label]=Pricing Matrix" \
  -F "configuration[labels][en_US]=Pricing Matrix" \
  -F "file=@dist/pricing_matrix.js;type=application/javascript"

Updating the custom variable via API

curl -X PATCH \
  "https://<your-instance>.cloud.akeneo.com/api/rest/v1/ui-extensions/<uuid>" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "configuration": {
      "custom_variables": [
        {
          "code": "subsidiary_price_attributes",
          "value": "AZ:price_az,BI:price_bi,..."
        }
      ]
    }
  }'

Deploying to a new Akeneo instance

  1. Confirm the attribute codes in the target instance. Browse to Settings → Attributes and find the pim_catalog_price_collection attributes you want to surface.
  2. Either set the subsidiary_price_attributes custom variable after deployment (Option A above), or update src/config.ts before building (Option B).
  3. Run npm run build.
  4. POST the bundle and manifest to the new instance using the First-time deployment command above with the new instance URL and a valid Bearer token.
  5. In the Akeneo UI, navigate to Settings → UI Extensions, find the extension, and confirm it is set to Active.
  6. Open any product that has the relevant price attributes assigned to its family — the Pricing Matrix panel should appear.

Gotchas

CDN caching. Akeneo CDN-caches the extension JS at the URL bound to the original upload. If a PATCH update does not appear to take effect (old JS still runs), the most reliable fix is to delete the extension and recreate it via a fresh POST. This forces a new asset URL that bypasses the cache.

Bearer tokens expire. Tokens issued via the Akeneo UI are short-lived. If API calls return 401, generate a new token under Settings → Users → → API.

Attribute must be in the product's family. If an attribute code is configured but not assigned to the product's family (or has no value set), the row displays "Attribute not set on this product" rather than prices. This is expected behaviour.

amount type. The Akeneo API returns amount as a string in most cases but occasionally as a bare number for whole-number values (e.g. 1 instead of "1.00"). The hook normalises this with String(entry.amount) before storing.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors