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.
- When a user opens a product, the extension reads the product UUID from
globalThis.PIM.context.product.uuid. - It fetches the full product record via the Akeneo API
(
PIM.api.product_uuid_v1.get). The SDK handles authentication automatically. - For each configured subsidiary, it looks up the matching attribute in
product.valuesand extracts the price entries (amount+currency). - Currency columns are derived dynamically from the data — no hardcoding required. EUR is always shown first; remaining currencies are sorted alphabetically.
- 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.
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
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.
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_codeseparated by commas. Labelis the display name shown in the table row (can be any string, no colons).attribute_codemust match an existing Akeneo attribute of typepim_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.
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.
npm install
npm run buildThe 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.
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.
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.
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"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,..."
}
]
}
}'- Confirm the attribute codes in the target instance. Browse to
Settings → Attributes and find the
pim_catalog_price_collectionattributes you want to surface. - Either set the
subsidiary_price_attributescustom variable after deployment (Option A above), or updatesrc/config.tsbefore building (Option B). - Run
npm run build. - 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.
- In the Akeneo UI, navigate to Settings → UI Extensions, find the extension, and confirm it is set to Active.
- Open any product that has the relevant price attributes assigned to its family — the Pricing Matrix panel should appear.
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.