magendoo / module-base-price
EU Base Price (Grundpreis) display for Magento 2. Shows price per reference unit (kg, litre, m) on product pages, category listings, search results and cart. Compliant with EU Price Indication Directive 98/6/EC and German PAngV.
Magendoo Base Price — Magento 2 Module
Displays a per-unit reference price (Grundpreis) next to every product's selling price across product pages, category listings, search results, and the shopping cart.
A customer buying a 300 g shampoo at €4.99 also sees "Base price: €16.63 / 1 kilogram", enabling direct price comparison across different pack sizes and brands.
Legal background
Displaying a unit reference price is legally required in several jurisdictions:
- Germany — Preisangabenverordnung (PAngV §4)
- EU member states — Price Indication Directive 98/6/EC, updated by Directive 2019/2161 (Omnibus)
The obligation applies to products sold by weight, volume, length, area, or piece count that are offered in different pack sizes. Consult your legal adviser to determine your specific requirements.
Requirements
| Dependency | Version |
|---|---|
| Magento Open Source / Adobe Commerce | 2.4.x |
| PHP | 8.1 or higher |
Installation
Via Composer (recommended)
composer require magendoo/module-base-price
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
Manual
mkdir -p app/code/Magendoo/BasePrice
# Copy all module files into that directory
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
setup:upgrade creates the magendoo_baseprice_unit table, adds four EAV product attributes, and seeds nine SI measurement units.
Configuration
Navigate to Stores → Configuration → Magendoo Extensions → Base Price.
General
| Field | Default | Description |
|---|---|---|
| Enable Base Price | Yes | Master switch. Disabling this hides base prices everywhere. |
Product Page
| Field | Default | Description |
|---|---|---|
| Enable on Product Pages | Yes | Show base price on individual product pages. |
| Display Template | Base price: {{unit_price}} / {{reference_amount}} {{reference_unit}} |
Output format. Supports all template variables. |
| CSS Class | base-price |
Class applied to the wrapping <div>. |
Category & Search
| Field | Default | Description |
|---|---|---|
| Enable on Category Pages | Yes | Show base price on category listing pages. |
| Category Display Template | ({{unit_price}} / {{reference_amount}} {{reference_unit}}) |
Output format for listing cards. |
| Enable on Search Results | Yes | Show base price on catalogsearch result pages. |
Cart
| Field | Default | Description |
|---|---|---|
| Enable on Cart Page | Yes | Show base price in the shopping cart item rows. |
| Cart Display Template | Base price: {{unit_price}} / {{reference_amount}} {{reference_unit}} |
Output format for cart rows. |
| Enable on Mini-Cart | Yes | Reserved for future Knockout.js integration. |
Template variables
All display templates accept the following placeholders:
| Variable | Description | Example output |
|---|---|---|
{{unit_price}} |
Formatted price per reference unit | €16.63 |
{{reference_amount}} |
Reference quantity | 1 |
{{reference_unit}} |
Reference unit label | kilogram |
{{product_amount}} |
Numeric amount in the product | 300 |
{{measure_unit}} |
Name of the product's measurement unit | gram |
{{product_price}} |
Selling price formatted via store currency | €4.99 |
Unknown placeholders are left intact, so you can safely mix in static text.
Formula
base_price = (selling_price ÷ product_amount) × (reference_amount × rate)
Where rate is the conversion factor from the product unit to the reference unit (e.g. 1 000 for gram → kilogram).
Examples:
| Product | Price | Amount | Unit | Calculation | Base price |
|---|---|---|---|---|---|
| Shampoo | €4.99 | 300 g | gram | (4.99 ÷ 300) × (1 × 1 000) | €16.63 / kg |
| Wine | €9.99 | 0.75 l | litre | (9.99 ÷ 0.75) × (1 × 1) | €13.32 / litre |
| Ribbon | €5.00 | 200 cm | centimetre | (5.00 ÷ 200) × (1 × 100) | €2.50 / metre |
| 6-pack | €5.94 | 6 | piece | (5.94 ÷ 6) × (1 × 1) | €0.99 / piece |
The result is rounded to 4 decimal places before formatting.
Reference amount override
A product can override the unit's default reference amount. Set Reference Amount to 0.1 on a gram-unit product to show the price per 100 g instead of per kilogram:
(4.99 ÷ 300) × (0.1 × 1 000) = €1.66 / 100 g
Unit management
Navigate to Catalog → Base Price Units in the admin panel.
Nine SI units are pre-installed:
| ID | Unit | Reference unit | Rate |
|---|---|---|---|
| 1 | gram | kilogram | 1 000 |
| 2 | milligram | kilogram | 1 000 000 |
| 3 | kilogram | kilogram | 1 |
| 4 | millilitre | litre | 1 000 |
| 5 | centilitre | litre | 100 |
| 6 | litre | litre | 1 |
| 7 | centimetre | metre | 100 |
| 8 | metre | metre | 1 |
| 9 | piece | piece | 1 |
Adding a custom unit
Click Add New Unit and fill in:
| Field | Description | Example |
|---|---|---|
| Name | Display name of the product unit | fluid ounce |
| Reference Unit Label | The reference unit shown in the output | litre |
| Rate | Multiplier to convert to the reference unit | 33.814 |
| Reference Amount | Default quantity shown in the output | 1 |
| Position | Sort order in the admin grid | 100 |
| Status | Active / Inactive | Active |
Configuring products
Admin UI
Open any product in the admin and locate the Base Price panel (below the standard price fields):
| Field | Description |
|---|---|
| Enable Base Price | Override the module-level enable/disable for this specific product. |
| Product Amount | Numeric quantity contained in the package (e.g. 300 for 300 g). |
| Unit | Select from the unit list (gram, millilitre, etc.). |
| Reference Amount | Leave blank to use the unit's default. Set a value to show a custom reference (e.g. 0.1 for "per 100 g"). |
REST API — set a product's base price
TOKEN=$(curl -s -X POST https://your-store.com/rest/V1/integration/admin/token \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"your-password"}' | tr -d '"')
# Set base price (300 g, gram unit = ID 1)
curl -X PUT https://your-store.com/rest/V1/baseprice/products/24-MB01 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"productBasePrice": {
"sku": "24-MB01",
"product_amount": 300,
"unit_id": 1
}
}'
# Enable it on the product
curl -X PUT https://your-store.com/rest/V1/products/24-MB01 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"product": {
"sku": "24-MB01",
"custom_attributes": [
{"attribute_code": "baseprice_is_enabled", "value": "1"}
]
}
}'
REST API reference
Units (admin token required)
| Method | Endpoint | Description |
|---|---|---|
| GET | /V1/baseprice/units |
List units (supports searchCriteria) |
| GET | /V1/baseprice/units/:unitId |
Get unit by ID |
| POST | /V1/baseprice/units |
Create a unit |
| PUT | /V1/baseprice/units/:unitId |
Update a unit |
| DELETE | /V1/baseprice/units/:unitId |
Delete a unit |
Create a unit:
curl -X POST https://your-store.com/rest/V1/baseprice/units \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"unit": {
"name": "fluid ounce",
"reference_unit_label": "litre",
"rate": 33.814,
"reference_amount": 1,
"position": 100,
"active": true
}
}'
Note: The active flag is sent as
active, notis_active. Magento's serialiser strips theisprefix from getter method names when building the response body.
Product base price (admin token required)
| Method | Endpoint | Description |
|---|---|---|
| GET | /V1/baseprice/products/:sku |
Get base price config for a product |
| PUT | /V1/baseprice/products/:sku |
Set base price config |
| DELETE | /V1/baseprice/products/:sku |
Remove base price config |
Calculator (anonymous)
GET /V1/baseprice/calculate?price=4.99&productAmount=300&unitId=1
Returns the raw base price as a float. Optional parameter: referenceAmountOverride.
curl 'https://your-store.com/rest/V1/baseprice/calculate?price=4.99&productAmount=300&unitId=1'
# → 16.6333
Architecture notes
- EAV attributes — Four product attributes (
baseprice_is_enabled,baseprice_product_amount,baseprice_unit_id,baseprice_reference_amount) are registered withused_in_product_listing = trueso they load in category and search collections. - Listing injection — A plugin on
Magento\Catalog\Block\Product\AbstractProduct::getProductDetailsHtml()appends the base price HTML to every product card, covering all product types without type-specific renderer registration. - Cart injection — The cart block is a child of Magento's
additional.product.infoblock. The cart item template iterates through its children and callssetItem()+toHtml()on each. - Product page — A standard
Templateblock is added viacatalog_product_view.xmland the product block is retrieved directly from layout. - Template rendering —
Model/TemplateRendererdoes a plainstr_replaceover{{variable}}placeholders. HTML escaping is the caller's responsibility;priceCurrency->format()handles currency formatting.
Releases and Packagist
Creating a release
Tag a version and push — the Release workflow runs tests then creates a GitHub release automatically:
git tag v1.2.0
git push origin v1.2.0
Version numbers follow Semantic Versioning. Update CHANGELOG.md and composer.json ("version") before tagging.
Publishing to Packagist
-
Go to packagist.org/packages/submit and enter:
https://github.com/magendooro/module-base-price -
After submission, enable auto-update so Packagist picks up new tags automatically:
- In the Packagist package settings, copy the webhook URL
- In the GitHub repository: Settings → Webhooks → Add webhook → paste the URL
- Content type:
application/json, events: Just the push event
Alternatively, connect your GitHub account to Packagist (Settings → Profile → GitHub) which handles webhooks automatically.
GitHub Secrets required for CI
The workflows install Magento packages from repo.magento.com, which requires Marketplace credentials. Add these to the repository as Actions secrets (Settings → Secrets and variables → Actions):
| Secret | Description |
|---|---|
MAGENTO_MARKETPLACE_PUBLIC_KEY |
Public key from marketplace.magento.com/customer/accessKeys |
MAGENTO_MARKETPLACE_PRIVATE_KEY |
Corresponding private key |
Without these secrets the CI runs will fail at the composer install step.
Running tests
PHPUnit (unit tests)
# From the Magento root
vendor/bin/phpunit --filter Magendoo\\BasePrice app/code/Magendoo/BasePrice/Test/Unit
Playwright (functional tests)
The functional test suite lives in dev/tests/functional/playwright/tests/baseprice/ and covers:
baseprice-calculate-combinations.spec.js— All nine seed units,referenceAmountOverride, edge cases, formula consistency (45 tests, no products or admin setup needed — hits the anonymous/V1/baseprice/calculateendpoint directly)baseprice-product-display.spec.js— Product detail page, category listing, cart page, disabled state (22 tests, sets up three sample products inbeforeAlland restores them inafterAll)
cd dev/tests/functional/playwright
npx playwright test tests/baseprice/
Uninstall
bin/magento module:uninstall Magendoo_BasePrice
bin/magento setup:upgrade
bin/magento cache:flush
This removes the magendoo_baseprice_unit table and the four EAV product attributes.
License
MIT — see LICENSE for details.
Changelog
All notable changes to this project will be documented in this file.
The format follows Keep a Changelog.
This project adheres to Semantic Versioning.
Unreleased
1.0.0 - 2026-04-13
Added
- Base price calculation and display on product detail pages, category listing pages, search result pages, and the shopping cart
- Formula:
(selling_price ÷ product_amount) × (reference_amount × rate), rounded to 4 decimal places - Nine pre-installed SI measurement units: gram, milligram, kilogram, millilitre, centilitre, litre, centimetre, metre, piece
- Configurable display templates with six
{{variable}}placeholders:unit_price,reference_amount,reference_unit,measure_unit,product_amount,product_price - Admin unit management grid at Catalog → Base Price Units (create, edit, delete, mass-enable/disable)
- Per-product enable/disable toggle and optional reference amount override (e.g. show per 100 g instead of per kg)
- REST API: unit CRUD (
/V1/baseprice/units), product base price read/write/delete (/V1/baseprice/products/:sku), and an anonymous calculator endpoint (/V1/baseprice/calculate) - EAV attributes
baseprice_is_enabled,baseprice_product_amount,baseprice_unit_id,baseprice_reference_amountwithused_in_product_listing = truefor efficient collection loading - 32 PHPUnit unit tests covering every calculation combination, reference amount override, edge cases, template rendering, and formatting
- PHPStan level 2 clean
Requires 8
| Package | Constraint |
|---|---|
| magento/framework | >=102.0.0 |
| magento/module-backend | * |
| magento/module-catalog | * |
| magento/module-config | * |
| magento/module-eav | * |
| magento/module-store | * |
| magento/module-ui | * |
| php | >=8.1 |
Requires-dev 2
| Package | Constraint |
|---|---|
| phpstan/phpstan | ^1.10 || ^2.0 |
| phpunit/phpunit | ^10.5 |
Compatibility
Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.
Code Quality
Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge. A phpcs finding can't make a module incompatible.
Static analysis
Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.
Tests
Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.
Unit tests
Integration tests
| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | ||
| 2.4.8 | N/A | N/A | ||
| 2.4.9 | N/A | N/A |
Security
Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.
More from magendoo
View vendorShipping Restrictions module
Product labels for Magento 2 - first-class label entities with stable codes, manual assignment via product attribute, rule-computed assignments materialized by an indexer, PLP/PDP badges and GraphQL exposure
EU Omnibus Directive (2019/2161) price tracker for Magento 2. Records price changes with timestamps, computes the lowest prior price in the disclosure window, and displays it on PDP and PLP, with async queue processing for bulk operations.
Catalog data quality observability for Magento 2 — completeness scoring, filter-blind detection, and gap reporting
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.