diff --git a/demoextrafield/README.md b/demoextrafield/README.md
new file mode 100644
index 0000000..5e5290d
--- /dev/null
+++ b/demoextrafield/README.md
@@ -0,0 +1,108 @@
+# Demo extra fields
+
+## About
+
+This module demonstrates how to use **native extra fields** (custom fields) in PrestaShop (9.2+ ?).
+
+It focuses on:
+
+- Registering extra fields on multiple entities (Product, Category, Customer)
+- Covering multiple **scopes** (`common`, `lang`, `shop`) and **types** (bool, date, money, html, json, url, …)
+- Unregistering extra fields on uninstall (including dropping the SQL storage columns)
+- Rendering the stored values on the Front Office using hooks
+- Making Back Office translation strings visible in the translation interface
+
+## What it registers
+
+### Product (`product`)
+
+- `is_dangerous` (scope: `common`, type: bool)
+- `video_link` (scope: `lang`, type: string/url)
+- `custom_date` (scope: `shop`, type: date)
+
+### Category (`category`)
+
+- `theme_color` (scope: `common`, type: string/color)
+- `marketing_note` (scope: `common`, type: html)
+- `id_supplier` (scope: `common`, type: int / supplier selector)
+
+### Customer (`customer`)
+
+- `credit_limit` (scope: `common`, type: float / money)
+- `extra_json` (scope: `common`, type: json)
+
+## How to test
+
+This module impacts both Back Office and Front Office.
+
+### Product
+
+**Back Office grid**
+
+- Adds a **"Dangerous product"** field displayed after **"Quantity"**.
+- Adds a **"Custom date"** field displayed at the end of the grid.
+- Toggling **"Dangerous product"** persists the value.
+
+**Back Office form**
+
+- Extra fields are grouped into a dedicated **"Extra fields"** tab.
+- Except **"Dangerous product"**, which is displayed at the end of the **"Options"** tab.
+
+**Front Office hooks**
+
+- Product page: `displayProductAdditionalInfo`
+- Cart: `displayCartExtraProductInfo`
+
+### Category
+
+**Back Office grid**
+
+- Adds **Theme color** and **Marketing note** at the end of the grid.
+
+**Back Office form**
+
+- Adds **Theme color** and **Marketing note** to the form.
+
+**Front Office hooks**
+
+- Category listing page: `displayHeaderCategory`
+
+### Customer
+
+**Back Office grid**
+
+- Adds **Credit limit** in the grid.
+
+**Back Office form**
+
+- Adds **Credit limit** and **Metadata JSON** to the form.
+
+**Front Office hooks**
+
+- My account page: `displayCustomerAccountTop`
+
+### Where to find values in FO templates
+
+On the Front Office, the module displays **only the values stored for this module**, under `extraProperties['demoextrafield']`.
+
+## Translation note (Back Office)
+
+Each extra field has a **title** and a **description** meant to be displayed in Back Office.
+The system stores the source wording and its translation domain (for the default language), then translations are managed through PrestaShop Back Office.
+
+To make those strings appear in the Back Office translation interface, two conditions must be met:
+
+1. The strings must be declared in PHP via `$this->trans(...)` (see `demoextrafield::registerTranslationWordings()`).
+2. The same source strings must exist at least once in an XLF file shipped by the module (see `translations/fr-FR/ModulesDemoextrafieldAdmin.fr-FR.xlf`).
+
+## Supported PrestaShop versions
+
+Compatible with 9.2 ? and above versions.
+
+## How to install
+
+1. Download or clone the module into the `modules` directory of your PrestaShop installation
+2. Install the module:
+ - from Back Office in Module Manager
+ - or using the command `php ./bin/console prestashop:module install demoextrafield`
+
diff --git a/demoextrafield/config.xml b/demoextrafield/config.xml
new file mode 100644
index 0000000..7e62df5
--- /dev/null
+++ b/demoextrafield/config.xml
@@ -0,0 +1,11 @@
+
+
+ demoextrafield
+
+
+
+
+
+ 0
+ 0
+
\ No newline at end of file
diff --git a/demoextrafield/config_fr.xml b/demoextrafield/config_fr.xml
new file mode 100644
index 0000000..7e62df5
--- /dev/null
+++ b/demoextrafield/config_fr.xml
@@ -0,0 +1,11 @@
+
+
+ demoextrafield
+
+
+
+
+
+ 0
+ 0
+
\ No newline at end of file
diff --git a/demoextrafield/demoextrafield.php b/demoextrafield/demoextrafield.php
new file mode 100644
index 0000000..743773d
--- /dev/null
+++ b/demoextrafield/demoextrafield.php
@@ -0,0 +1,566 @@
+name = 'demoextrafield';
+ $this->tab = 'administration';
+ $this->version = '1.0.0';
+ $this->author = 'PrestaShop';
+ $this->need_instance = 0;
+ $this->ps_versions_compliancy = ['min' => '9.2.0', 'max' => '9.9.99'];
+
+ parent::__construct();
+
+ $this->displayName = 'Demo native extra fields';
+ $this->description = 'Example module showing how to register and display native extra fields.';
+ }
+
+ /**
+ * Install:
+ * - registers extra fields for product/category/customer,
+ * - registers a few FO hooks to display values.
+ */
+ public function install(): bool
+ {
+ if (!parent::install()) {
+ return false;
+ }
+
+ /**
+ * PRODUCT extra fields
+ */
+
+ // Product (common) : is_dangerous
+ $productDangerousRegistered = $this->registerExtraProperty(
+ 'product',
+ 'is_dangerous',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::BOOL,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: false,
+ defaultValue: 0,
+ labelWording: $this->trans('Dangerous product', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Indicates whether the product is dangerous', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: CheckboxType::class,
+ validator: 'isBool',
+ displayApi: true,
+ associatedForms: ['product.options.extra_properties'],
+ associatedGrids: ['product.reference'],
+ )
+ );
+ if (!$productDangerousRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Product extra field "is_dangerous" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Product (lang) : video_link
+ $productVideoLinkRegistered = $this->registerExtraProperty(
+ 'product',
+ 'video_link',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::STRING,
+ scope: ExtraPropertyScope::LANG,
+ nullable: true,
+ labelWording: $this->trans('Video link', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Video URL per language', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ sqlIndex: ExtraPropertySqlIndex::UNIQUE,
+ formFieldType: UrlType::class,
+ validator: 'isUrl',
+ displayApi: true,
+ associatedForms: ['product'],
+ )
+ );
+ if (!$productVideoLinkRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Product extra field "video_link" (scope: lang).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Product (shop) : custom_date
+ $productCustomDateRegistered = $this->registerExtraProperty(
+ 'product',
+ 'custom_date',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::DATE,
+ scope: ExtraPropertyScope::SHOP,
+ nullable: true,
+ labelWording: $this->trans('Custom date', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Custom date per shop', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ sqlIndex: ExtraPropertySqlIndex::KEY,
+ formFieldType: DatePickerType::class,
+ validator: 'isDate',
+ displayApi: true,
+ associatedForms: ['product'],
+ associatedGrids: ['product.final_price_tax_excluded:before'],
+ )
+ );
+ if (!$productCustomDateRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Product extra field "custom_date" (scope: shop).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Product (common) : date_last_seen
+ // Auto-updated on each FO product page view (hookDisplayFooterProduct).
+ // displayForm: false → read-only for merchants; visible in the product grid and via API.
+ $productDateLastSeenRegistered = $this->registerExtraProperty(
+ 'product',
+ 'date_last_seen',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::DATE,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Date last seen', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Last time this product page was viewed', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ displayApi: true,
+ associatedGrids: ['product'],
+ )
+ );
+ if (!$productDateLastSeenRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Product extra field "date_last_seen" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Product (common) : packaging_type
+ // Demonstrates: CHOICE type, enumValues, formOptions (dropdown choices).
+ // enumValues constrains the allowed DB values; formOptions drives the Symfony ChoiceType widget.
+ // formRequired: false + nullable: true + placeholder → the "—" option represents "no selection";
+ // the field can be left empty and the empty value passes server-side validation.
+ // (For a truly required field, omit the placeholder and set formRequired: true — NotBlank is added automatically.)
+ $productPackagingTypeRegistered = $this->registerExtraProperty(
+ 'product',
+ 'packaging_type',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::CHOICE,
+ scope: ExtraPropertyScope::COMMON,
+ enumValues: ['standard', 'gift', 'bulk'],
+ nullable: true,
+ defaultValue: null,
+ labelWording: $this->trans('Packaging type', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Selectable packaging type for this product', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: ChoiceType::class,
+ formOptions: [
+ 'choices' => [
+ 'Standard' => 'standard',
+ 'Gift box' => 'gift',
+ 'Bulk' => 'bulk',
+ ],
+ 'placeholder' => '—',
+ ],
+ formRequired: false,
+ displayApi: true,
+ displayFront: true,
+ associatedForms: ['product'],
+ associatedGrids: ['product'],
+ )
+ );
+ if (!$productPackagingTypeRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Product extra field "packaging_type" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ /**
+ * CATEGORY extra fields
+ */
+
+ // Category (common) : theme_color
+ // Demonstrates: formRequired: true → the form modifier automatically adds a NotBlank
+ // constraint at build time (server-side enforcement, not just the HTML required attribute).
+ // No need to put constraints in formOptions — formOptions is persisted as JSON and cannot
+ // hold Constraint objects.
+ $categoryThemeColorRegistered = $this->registerExtraProperty(
+ 'category',
+ 'theme_color',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::STRING,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Theme color', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Color associated with the category (required)', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: ColorType::class,
+ formRequired: true,
+ validator: 'isColor',
+ displayApi: true,
+ associatedForms: ['category', 'root_category'],
+ associatedGrids: ['category']
+ )
+ );
+ if (!$categoryThemeColorRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Category extra field "theme_color" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Category (common) : marketing_note
+ $categoryMarketingNoteRegistered = $this->registerExtraProperty(
+ 'category',
+ 'marketing_note',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::HTML,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Marketing note', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Free note displayed in BO, API and FO', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: FormattedTextareaType::class,
+ validator: 'isCleanHtml',
+ displayApi: true,
+ associatedForms: ['category'],
+ )
+ );
+ if (!$categoryMarketingNoteRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Category extra field "marketing_note" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Category (common) : id_supplier
+ $categorySupplierRegistered = $this->registerExtraProperty(
+ 'category',
+ 'id_supplier',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::INT,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Default supplier', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Select a PrestaShop supplier', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: DiscountSupplierType::class,
+ validator: 'isUnsignedId',
+ displayApi: true,
+ associatedForms: ['category'],
+ associatedGrids: ['category']
+ )
+ );
+ if (!$categorySupplierRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Category extra field "id_supplier" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ /**
+ * CUSTOMER extra fields
+ */
+
+ // Customer (common) : credit_limit
+ $customerCreditLimitRegistered = $this->registerExtraProperty(
+ 'customer',
+ 'credit_limit',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::FLOAT,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Credit limit', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Maximum customer credit amount', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: MoneyType::class,
+ validator: 'isPrice',
+ displayApi: true,
+ associatedForms: ['customer'],
+ associatedGrids: ['customer']
+ )
+ );
+ if (!$customerCreditLimitRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Customer extra field "credit_limit" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Customer (common) : extra_json
+ $customerExtraJsonRegistered = $this->registerExtraProperty(
+ 'customer',
+ 'extra_json',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::JSON,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Metadata JSON', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Free JSON for customer metadata', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: TextareaType::class,
+ validator: 'isJson',
+ displayApi: true,
+ associatedForms: ['customer'],
+ )
+ );
+ if (!$customerExtraJsonRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Customer extra field "extra_json" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ // Customer (common) : internal_note
+ // Demonstrates: displayFront: false — the field appears in BO form and API but is
+ // never returned by ExtraPropertiesLazyArray::getValues() on the front office.
+ $customerInternalNoteRegistered = $this->registerExtraProperty(
+ 'customer',
+ 'internal_note',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::STRING,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ labelWording: $this->trans('Internal note', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Merchant-only note — never exposed on the front office', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: TextareaType::class,
+ displayApi: true,
+ displayFront: false,
+ associatedForms: ['customer'],
+ )
+ );
+ if (!$customerInternalNoteRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Customer extra field "internal_note" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ /**
+ * ADDRESS extra fields
+ *
+ * Demo case: gridId ('manufacturer_address') differs from entity name ('address').
+ * This validates that getDefinitionCollectionByGridId() correctly decouples
+ * the grid identifier from the entity table name.
+ *
+ * Note on displayForm:
+ * The form modifier resolves extra fields using the form type's block prefix as the entity
+ * name. ManufacturerAddressType has block_prefix='manufacturer_address', but the entity
+ * table is 'address'. Because block_prefix ≠ entity_name, the form modifier cannot find
+ * definitions registered for 'address' when building the 'manufacturer_address' form.
+ * displayForm is therefore set to false: the field is intentionally grid-only here.
+ * (Forms where block_prefix == entity_name — e.g. product, customer, category — work
+ * correctly without this constraint.)
+ */
+
+ // Address (common) : delivery_note
+ // Shows in the manufacturer address grid (Catalog > Brands > Addresses) after 'city'.
+ $addressDeliveryNoteRegistered = $this->registerExtraProperty(
+ 'address',
+ 'delivery_note',
+ new ExtraPropertyDefinition(
+ type: ExtraPropertyType::STRING,
+ scope: ExtraPropertyScope::COMMON,
+ nullable: true,
+ size: 255,
+ labelWording: $this->trans('Delivery note', [], 'Modules.Demoextrafield.Admin', 'en'),
+ labelDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: $this->trans('Free delivery note attached to this address', [], 'Modules.Demoextrafield.Admin', 'en'),
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ formFieldType: TextareaType::class,
+ validator: 'isGenericName',
+ displayApi: true,
+ // gridId 'manufacturer_address' ≠ entity 'address' — decoupling test.
+ associatedGrids: ['manufacturer_address.city'],
+ )
+ );
+ if (!$addressDeliveryNoteRegistered) {
+ $this->_errors[] = $this->trans('Failed to register Address extra field "delivery_note" (scope: common).', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ $hooksRegistered = $this->registerHook('displayProductAdditionalInfo')
+ && $this->registerHook('displayCartExtraProductInfo')
+ && $this->registerHook('displayHeaderCategory')
+ && $this->registerHook('displayCustomerAccountTop')
+ && $this->registerHook('displayFooterProduct');
+ if (!$hooksRegistered) {
+ $this->_errors[] = $this->trans('Failed to register one or more hooks.', [], 'Modules.Demoextrafield.Admin');
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Uninstall:
+ * - unregisters all extra fields,
+ * - drops SQL storage columns.
+ */
+ public function uninstall(): bool
+ {
+ // false = keep columns in DB after uninstall
+ $dropColumn = true;
+
+ return
+ $this->unregisterExtraProperty('product', 'video_link', ExtraPropertyScope::LANG, $dropColumn)
+ && $this->unregisterExtraProperty('product', 'is_dangerous', ExtraPropertyScope::COMMON, $dropColumn)
+ && $this->unregisterExtraProperty('product', 'custom_date', ExtraPropertyScope::SHOP, $dropColumn)
+ && $this->unregisterExtraProperty('product', 'date_last_seen', ExtraPropertyScope::COMMON, $dropColumn)
+ && $this->unregisterExtraProperty('product', 'packaging_type', ExtraPropertyScope::COMMON, $dropColumn)
+
+ && $this->unregisterExtraProperty('category', 'theme_color', ExtraPropertyScope::COMMON, $dropColumn)
+ && $this->unregisterExtraProperty('category', 'marketing_note', ExtraPropertyScope::COMMON, $dropColumn)
+ && $this->unregisterExtraProperty('category', 'id_supplier', ExtraPropertyScope::COMMON, $dropColumn)
+
+ && $this->unregisterExtraProperty('customer', 'credit_limit', ExtraPropertyScope::COMMON, $dropColumn)
+ && $this->unregisterExtraProperty('customer', 'extra_json', ExtraPropertyScope::COMMON, $dropColumn)
+ && $this->unregisterExtraProperty('customer', 'internal_note', ExtraPropertyScope::COMMON, $dropColumn)
+
+ && $this->unregisterExtraProperty('address', 'delivery_note', ExtraPropertyScope::COMMON, $dropColumn)
+
+ && parent::uninstall();
+ }
+
+ /**
+ * Front Office hook (product page).
+ * Displays this module extra fields from the product LazyArray.
+ */
+ public function hookDisplayProductAdditionalInfo(array $params): string
+ {
+ return $this->display(__FILE__, 'views/templates/hook/product_additional_info.tpl');
+ }
+
+ /**
+ * Front Office hook (product page footer).
+ *
+ * Demo: reads date_last_seen from the Product ObjectModel, displays it, then updates it.
+ *
+ * Access is grouped by module: $product->extra_properties['module']['field']
+ */
+ public function hookDisplayFooterProduct(array $params): string
+ {
+ $productId = (int) ($params['product']['id_product'] ?? 0);
+ if ($productId <= 0) {
+ return '';
+ }
+
+ $product = new Product($productId);
+ if (!Validate::isLoadedObject($product)) {
+ return '';
+ }
+
+ $now = date('Y-m-d H:i:s');
+
+ $dateLastSeen = $product->extra_properties['demoextrafield']['date_last_seen'];
+ $product->extra_properties['demoextrafield']['date_last_seen'] = $now;
+ $product->update();
+
+ $this->context->smarty->assign([
+ 'dateLastSeen' => $dateLastSeen,
+ 'dateLastSeenUpdated' => $now,
+ ]);
+
+ return $this->display(__FILE__, 'views/templates/hook/product_footer.tpl');
+ }
+
+ /**
+ * Front Office hook (cart).
+ * Displays this module extra fields for products in cart.
+ *
+ * $params['product'] is the product LazyArray passed by the cart template.
+ */
+ public function hookDisplayCartExtraProductInfo(array $params): string
+ {
+ $this->context->smarty->assign('product', $params['product'] ?? []);
+
+ return $this->display(__FILE__, 'views/templates/hook/cart_extra_product_info.tpl');
+ }
+
+ /**
+ * Front Office hook (category listing page).
+ * Displays this module extra fields from the category LazyArray.
+ */
+ public function hookDisplayHeaderCategory(): string
+ {
+ return $this->display(__FILE__, 'views/templates/hook/category_header.tpl');
+ }
+
+ /**
+ * Front Office hook (customer my-account page).
+ *
+ * --- Why we load extra properties manually here ---
+ *
+ * Extra properties are natively carried by every ObjectModel subclass (Customer, Product,
+ * Category…). In PHP, $customer->extra_properties['module']['field'] works on any instance.
+ *
+ * In FO Smarty templates however, entities are presented through LazyArrays
+ * (ProductLazyArray, CategoryLazyArray, OrderLazyArray…). AbstractLazyArray exposes the
+ * `extraProperties` key, so `$product.extraProperties.mymodule.myfield` works in templates.
+ *
+ * Customer is the exception: the Smarty `$customer` variable is a plain PHP array built
+ * by FrontController::getTemplateVarCustomer() via objectPresenter->present(). It is not
+ * a LazyArray, so `$customer.extraProperties` does not exist.
+ *
+ * Solution for entities without a native LazyArray: fetch the values explicitly in the
+ * hook handler using ExtraPropertiesLazyArray::fromObjectModelClass(), then assign them
+ * to Smarty under a dedicated variable.
+ *
+ * Note: getValues() already filters out fields with displayFront=false
+ * (here, 'internal_note' field is therefore intentionally absent from the output).
+ */
+ public function hookDisplayCustomerAccountTop(): string
+ {
+ $customerId = (int) $this->context->customer->id;
+ if ($customerId <= 0) {
+ return '';
+ }
+
+ $extraPropertiesByModule = ExtraPropertiesLazyArray::fromObjectModelClass(
+ Customer::class,
+ $customerId
+ )->getValues();
+
+ // Wrap as ['extra_properties' => ...] so _extra_properties.tpl can be reused as-is.
+ $this->context->smarty->assign('customerExtraData', ['extra_properties' => $extraPropertiesByModule]);
+
+ return $this->display(__FILE__, 'views/templates/hook/customer_account_top.tpl');
+ }
+}
diff --git a/demoextrafield/index.php b/demoextrafield/index.php
new file mode 100644
index 0000000..a9295d4
--- /dev/null
+++ b/demoextrafield/index.php
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Color associated with the category (required)
+ Couleur associée à la catégorie (obligatoire)
+
+
+ Credit limit
+ Limite de crédit
+
+
+ Custom date
+ Date personnalisée
+
+
+ Custom date per shop
+ Date personnalisée par boutique
+
+
+ Dangerous product
+ Produit dangereux
+
+
+ Date last seen
+ Date de dernière consultation
+
+
+ Date last seen (extra field demo)
+ Date de dernière consultation (démo champ supplémentaire)
+
+
+ Default supplier
+ Fournisseur par défaut
+
+
+ Delivery note
+ Note de livraison
+
+
+ Failed to register Address extra field "delivery_note" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Adresse "delivery_note" (portée : common).
+
+
+ Failed to register Category extra field "id_supplier" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Catégorie "id_supplier" (portée : common).
+
+
+ Failed to register Category extra field "marketing_note" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Catégorie "marketing_note" (portée : common).
+
+
+ Failed to register Category extra field "theme_color" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Catégorie "theme_color" (portée : common).
+
+
+ Failed to register Customer extra field "credit_limit" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Client "credit_limit" (portée : common).
+
+
+ Failed to register Customer extra field "extra_json" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Client "extra_json" (portée : common).
+
+
+ Failed to register Customer extra field "internal_note" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Client "internal_note" (portée : common).
+
+
+ Failed to register Product extra field "custom_date" (scope: shop).
+ Échec de l’enregistrement du champ supplémentaire Produit "custom_date" (portée : shop).
+
+
+ Failed to register Product extra field "date_last_seen" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Produit "date_last_seen" (portée : shop).
+
+
+ Failed to register Product extra field "is_dangerous" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Produit "is_dangerous" (portée : shop).
+
+
+ Failed to register Product extra field "packaging_type" (scope: common).
+ Échec de l’enregistrement du champ supplémentaire Produit "packaging_type" (portée : shop).
+
+
+ Failed to register Product extra field "video_link" (scope: lang).
+ Échec de l’enregistrement du champ supplémentaire Produit "video_link" (portée : lang).
+
+
+ Failed to register one or more hooks.
+ Échec de l’enregistrement d’un ou plusieurs hooks.
+
+
+ Free JSON for customer metadata
+ JSON libre pour les métadonnées client
+
+
+ Free delivery note attached to this address
+ Note de livraison libre associée à cette adresse
+
+
+ Free note displayed in BO, API and FO
+ Note libre affichée dans le BO, l’API et le FO
+
+
+ Indicates whether the product is dangerous
+ Indique si le produit est dangereux
+
+
+ Internal note
+ Note interne
+
+
+ Last time this product page was viewed
+ Dernière consultation de cette fiche produit
+
+
+ Marketing note
+ Note marketing
+
+
+ Maximum customer credit amount
+ Montant maximal de crédit client
+
+
+ Merchant-only note — never exposed on the front office
+ Note réservée au marchand — jamais affichée sur le front-office
+
+
+ Metadata JSON
+ JSON des métadonnées
+
+
+ Never seen before
+ Jamais consulté auparavant
+
+
+ Packaging type
+ Type d’emballage
+
+
+ Previous value
+ Valeur précédente
+
+
+ Select a PrestaShop supplier
+ Sélectionnez un fournisseur PrestaShop
+
+
+ Selectable packaging type for this product
+ Type d’emballage sélectionnable pour ce produit
+
+
+ Theme color
+ Couleur du thème
+
+
+ Updated to
+ Mis à jour vers
+
+
+ Video URL per language
+ URL de la vidéo par langue
+
+
+ Video link
+ Lien vidéo
+
+
+ ⚠ This product is marked as dangerous.
+ ⚠ Ce produit est marqué comme dangereux.
+
+
+
+
diff --git a/demoextrafield/translations/fr-FR/ModulesDemoextrafieldMain.fr-FR.xlf b/demoextrafield/translations/fr-FR/ModulesDemoextrafieldMain.fr-FR.xlf
new file mode 100644
index 0000000..f647c11
--- /dev/null
+++ b/demoextrafield/translations/fr-FR/ModulesDemoextrafieldMain.fr-FR.xlf
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Extra fields (demoextrafield)
+ Champs extra (demoextrafield)
+
+
+ No extra fields found for this module.
+ Aucun champ extra pour ce module.
+
+
+
+
diff --git a/demoextrafield/views/templates/hook/_extra_properties.tpl b/demoextrafield/views/templates/hook/_extra_properties.tpl
new file mode 100644
index 0000000..68c31d9
--- /dev/null
+++ b/demoextrafield/views/templates/hook/_extra_properties.tpl
@@ -0,0 +1,26 @@
+{*
+ Displays all extra fields registered by this module for a given entity.
+
+ Usage: {include file='./_extra_properties.tpl' objectModel=$product}
+ where $objectModel is a LazyArray (or any array) exposing extra_properties.{moduleName}.{fieldName}.
+
+ Note on lang-scoped fields (scope="lang"):
+ The ExtraPropertyReader translates the per-language array into a single scalar value for the
+ current storefront language before returning it. No special handling is needed here.
+
+ Note on displayFront=false fields:
+ Fields registered with displayFront=false are filtered out by ExtraPropertiesLazyArray::getValues()
+ before this template is reached. They will never appear here even if they exist in the database.
+*}
+{if empty($objectModel.extra_properties.demoextrafield)}
+
{l s='No extra fields found for this module.' d='Modules.Demoextrafield.Main'}
+{else}
+
+ {foreach from=$objectModel.extra_properties.demoextrafield key=fieldName item=fieldValue}
+
+ {$fieldName|escape:'htmlall':'UTF-8'}:
+ {$fieldValue|escape:'htmlall':'UTF-8'}
+
+ {/foreach}
+
+{/if}
diff --git a/demoextrafield/views/templates/hook/cart_extra_product_info.tpl b/demoextrafield/views/templates/hook/cart_extra_product_info.tpl
new file mode 100644
index 0000000..b25a251
--- /dev/null
+++ b/demoextrafield/views/templates/hook/cart_extra_product_info.tpl
@@ -0,0 +1,6 @@
+
+
diff --git a/demoextrafield/views/templates/hook/category_header.tpl b/demoextrafield/views/templates/hook/category_header.tpl
new file mode 100644
index 0000000..e47e9a1
--- /dev/null
+++ b/demoextrafield/views/templates/hook/category_header.tpl
@@ -0,0 +1,5 @@
+
diff --git a/demoextrafield/views/templates/hook/customer_account_top.tpl b/demoextrafield/views/templates/hook/customer_account_top.tpl
new file mode 100644
index 0000000..1498930
--- /dev/null
+++ b/demoextrafield/views/templates/hook/customer_account_top.tpl
@@ -0,0 +1,7 @@
+
diff --git a/demoextrafield/views/templates/hook/product_additional_info.tpl b/demoextrafield/views/templates/hook/product_additional_info.tpl
new file mode 100644
index 0000000..9bca1e4
--- /dev/null
+++ b/demoextrafield/views/templates/hook/product_additional_info.tpl
@@ -0,0 +1,14 @@
+
diff --git a/demoextrafield/views/templates/hook/product_footer.tpl b/demoextrafield/views/templates/hook/product_footer.tpl
new file mode 100644
index 0000000..beba2c8
--- /dev/null
+++ b/demoextrafield/views/templates/hook/product_footer.tpl
@@ -0,0 +1,17 @@
+