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_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..65916f3
--- /dev/null
+++ b/demoextrafield/demoextrafield.php
@@ -0,0 +1,498 @@
+name = 'demoextrafield';
+ $this->tab = 'administration';
+ $this->version = '1.0.0';
+ $this->author = 'PrestaShop';
+ $this->need_instance = 0;
+ $this->ps_versions_compliancy = ['min' => '9.1.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()) {
+ $errors = array_filter($this->getErrors());
+ $details = empty($errors) ? 'no legacy errors were provided' : implode(' | ', $errors);
+
+ throw new Exception(sprintf('demoextrafield: parent::install() failed (%s).', $details));
+ }
+
+ // IMPORTANT NOTE ON TRANSLATION:
+ // Each extra field has a title + description meant to be displayed in Back Office.
+ // We store the source wording + its translation domain (for the default language), then
+ // PrestaShop handles translations for active languages via the BO translation system.
+ //
+ // For those strings to show up in the BO translation interface, two conditions must be met:
+ // - the strings must be declared in PHP via $this->trans(...),
+ // - the same source strings must exist at least once in an XLF file shipped by the module.
+ $this->registerTranslationWordings();
+
+ /**
+ * PRODUCT extra fields
+ */
+
+ // Product (common) : is_dangerous
+ $productDangerousRegistered = $this->registerExtraProperty(
+ 'product',
+ 'is_dangerous',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::Bool,
+ scope: ExtraPropertyScope::Common,
+ nullable: false,
+ defaultValue: 0,
+ titleWording: 'Dangerous product',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Indicates whether the product is dangerous',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ symfonyFieldType: CheckboxType::class,
+ validator: 'isBool',
+ displayFront: false,
+ displayApi: true,
+ displayBo: true,
+ propertyPath: 'options.extra_properties',
+ displayGrid: true,
+ gridPosition: 'quantity'
+ )
+ );
+ if (!$productDangerousRegistered) {
+ $this->_errors[] = 'Failed to register Product extra field "is_dangerous" (scope: common).';
+
+ return false;
+ }
+
+ // Product (lang) : video_link
+ $productVideoLinkRegistered = $this->registerExtraProperty(
+ 'product',
+ 'video_link',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::String,
+ scope: ExtraPropertyScope::Lang,
+ nullable: true,
+ titleWording: 'Video link',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Video URL per language',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ sqlIndex: 'unique',
+ symfonyFieldType: UrlType::class,
+ validator: 'isUrl',
+ displayFront: true,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: false
+ )
+ );
+ if (!$productVideoLinkRegistered) {
+ $this->_errors[] = 'Failed to register Product extra field "video_link" (scope: lang).';
+
+ return false;
+ }
+
+ // Product (shop) : custom_date
+ $productCustomDateRegistered = $this->registerExtraProperty(
+ 'product',
+ 'custom_date',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::Date,
+ scope: ExtraPropertyScope::Shop,
+ nullable: true,
+ titleWording: 'Custom date',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Custom date per shop',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ sqlIndex: 'key',
+ symfonyFieldType: DatePickerType::class,
+ validator: 'isDate',
+ displayFront: false,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: true,
+ gridPosition: 3
+ )
+ );
+ if (!$productCustomDateRegistered) {
+ $this->_errors[] = 'Failed to register Product extra field "custom_date" (scope: shop).';
+
+ return false;
+ }
+
+ /**
+ * CATEGORY extra fields
+ */
+
+ // Category (common) : theme_color
+ $categoryThemeColorRegistered = $this->registerExtraProperty(
+ 'category',
+ 'theme_color',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::String,
+ scope: ExtraPropertyScope::Common,
+ nullable: true,
+ titleWording: 'Theme color',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Color associated with the category',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ symfonyFieldType: ColorType::class,
+ validator: 'isColor',
+ displayFront: true,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: true
+ )
+ );
+ if (!$categoryThemeColorRegistered) {
+ $this->_errors[] = 'Failed to register Category extra field "theme_color" (scope: common).';
+
+ return false;
+ }
+
+ // Category (common) : marketing_note
+ $categoryMarketingNoteRegistered = $this->registerExtraProperty(
+ 'category',
+ 'marketing_note',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::Html,
+ scope: ExtraPropertyScope::Common,
+ nullable: true,
+ titleWording: 'Marketing note',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Free note displayed in BO, API and FO',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ symfonyFieldType: FormattedTextareaType::class,
+ validator: 'isCleanHtml',
+ displayFront: true,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: false
+ )
+ );
+ if (!$categoryMarketingNoteRegistered) {
+ $this->_errors[] = 'Failed to register Category extra field "marketing_note" (scope: common).';
+
+ return false;
+ }
+
+ // Category (common) : id_supplier
+ $categorySupplierRegistered = $this->registerExtraProperty(
+ 'category',
+ 'id_supplier',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::Int,
+ scope: ExtraPropertyScope::Common,
+ nullable: true,
+ titleWording: 'Default supplier',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Select a PrestaShop supplier',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ symfonyFieldType: DiscountSupplierType::class,
+ validator: 'isUnsignedId',
+ displayFront: true,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: true
+ )
+ );
+ if (!$categorySupplierRegistered) {
+ $this->_errors[] = 'Failed to register Category extra field "id_supplier" (scope: common).';
+
+ return false;
+ }
+
+ /**
+ * CUSTOMER extra fields
+ */
+
+ // Customer (common) : credit_limit
+ $customerCreditLimitRegistered = $this->registerExtraProperty(
+ 'customer',
+ 'credit_limit',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::Float,
+ scope: ExtraPropertyScope::Common,
+ nullable: true,
+ titleWording: 'Credit limit',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Maximum customer credit amount',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ symfonyFieldType: MoneyType::class,
+ validator: 'isPrice',
+ displayFront: false,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: true
+ )
+ );
+ if (!$customerCreditLimitRegistered) {
+ $this->_errors[] = 'Failed to register Customer extra field "credit_limit" (scope: common).';
+
+ return false;
+ }
+
+ // Customer (common) : extra_json
+ $customerExtraJsonRegistered = $this->registerExtraProperty(
+ 'customer',
+ 'extra_json',
+ new ExtraPropertyOptions(
+ type: ExtraPropertyType::Json,
+ scope: ExtraPropertyScope::Common,
+ nullable: true,
+ titleWording: 'Metadata JSON',
+ titleDomain: self::TRANSLATION_DOMAIN,
+ descriptionWording: 'Free JSON for customer metadata',
+ descriptionDomain: self::TRANSLATION_DOMAIN,
+ symfonyFieldType: TextareaType::class,
+ validator: 'isJson',
+ displayFront: true,
+ displayApi: true,
+ displayBo: true,
+ displayGrid: false
+ )
+ );
+ if (!$customerExtraJsonRegistered) {
+ $this->_errors[] = 'Failed to register Customer extra field "extra_json" (scope: common).';
+
+ return false;
+ }
+
+ $hooksRegistered = $this->registerHook('displayProductAdditionalInfo')
+ && $this->registerHook('displayCartExtraProductInfo')
+ && $this->registerHook('displayHeaderCategory')
+ && $this->registerHook('displayCustomerAccountTop');
+ if (!$hooksRegistered) {
+ $this->_errors[] = 'Failed to register one or more hooks (displayProductAdditionalInfo, displayCartExtraProductInfo, displayHeaderCategory, displayCustomerAccountTop).';
+
+ 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;
+
+ $this->unregisterExtraProperty('product', 'video_link', 'lang', $dropColumn);
+ $this->unregisterExtraProperty('product', 'is_dangerous', 'common', $dropColumn);
+ $this->unregisterExtraProperty('product', 'custom_date', 'shop', $dropColumn);
+
+ $this->unregisterExtraProperty('category', 'theme_color', 'common', $dropColumn);
+ $this->unregisterExtraProperty('category', 'marketing_note', 'common', $dropColumn);
+ $this->unregisterExtraProperty('category', 'id_supplier', 'common', $dropColumn);
+
+ $this->unregisterExtraProperty('customer', 'credit_limit', 'common', $dropColumn);
+ $this->unregisterExtraProperty('customer', 'extra_json', 'common', $dropColumn);
+
+ return parent::uninstall();
+ }
+
+ /**
+ * Front Office hook (product page).
+ * Displays this module extra fields from the product LazyArray.
+ */
+ public function hookDisplayProductAdditionalInfo(array $params): string
+ {
+ $product = $params['product'] ?? null;
+ if (!$product instanceof ArrayAccess || (int) ($product['id_product'] ?? 0) <= 0) {
+ return '';
+ }
+
+ $moduleExtras = $product['extraProperties'][$this->name] ?? [];
+ if (!is_array($moduleExtras) && !$moduleExtras instanceof ArrayAccess) {
+ $moduleExtras = [];
+ }
+
+ $this->context->smarty->assign([
+ 'demoExtraFieldTitle' => $this->trans('Extra fields (demoextrafield)', [], self::TRANSLATION_DOMAIN),
+ 'entityLabel' => $this->trans('Entity', [], self::TRANSLATION_DOMAIN),
+ 'entityName' => 'product',
+ 'moduleExtras' => $moduleExtras,
+ ]);
+
+ return $this->display(__FILE__, 'views/templates/hook/product_additional_info.tpl');
+ }
+
+ /**
+ * Front Office hook (cart).
+ * Displays this module extra fields for products in cart.
+ */
+ public function hookDisplayCartExtraProductInfo(array $params): string
+ {
+ $product = $params['product'] ?? null;
+ if (!$product instanceof ArrayAccess || (int) ($product['id_product'] ?? 0) <= 0) {
+ return '';
+ }
+
+ $moduleExtras = $product['extraProperties'][$this->name] ?? [];
+ if (!is_array($moduleExtras) && !$moduleExtras instanceof ArrayAccess) {
+ $moduleExtras = [];
+ }
+
+ $this->context->smarty->assign([
+ 'demoExtraFieldTitle' => $this->trans('Extra fields (demoextrafield)', [], self::TRANSLATION_DOMAIN),
+ 'entityLabel' => $this->trans('Entity', [], self::TRANSLATION_DOMAIN),
+ 'entityName' => 'product',
+ 'moduleExtras' => $moduleExtras,
+ ]);
+
+ 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
+ {
+ $category = $this->context->smarty->getTemplateVars('category');
+ if (!$category instanceof ArrayAccess || (int) ($category['id_category'] ?? 0) <= 0) {
+ return '';
+ }
+
+ $moduleExtras = $category['extraProperties'][$this->name] ?? [];
+ if (!is_array($moduleExtras) && !$moduleExtras instanceof ArrayAccess) {
+ $moduleExtras = [];
+ }
+
+ $this->context->smarty->assign([
+ 'demoExtraFieldTitle' => $this->trans('Extra fields (demoextrafield)', [], self::TRANSLATION_DOMAIN),
+ 'entityLabel' => $this->trans('Entity', [], self::TRANSLATION_DOMAIN),
+ 'entityName' => 'category',
+ 'moduleExtras' => $moduleExtras,
+ ]);
+
+ return $this->display(__FILE__, 'views/templates/hook/category_header.tpl');
+ }
+
+ /**
+ * Front Office hook (customer my-account page).
+ * Displays this module extra fields for current customer.
+ */
+ public function hookDisplayCustomerAccountTop(): string
+ {
+ $customer = $this->context->customer;
+ if (!$customer instanceof Customer || (int) $customer->id <= 0) {
+ return '';
+ }
+
+ try {
+ $containerFinder = new ContainerFinder($this->context);
+ /** @var ExtraPropertyValueProviderInterface $extraPropertyValueProvider */
+ $extraPropertyValueProvider = $containerFinder->getContainer()->get(ExtraPropertyValueProviderInterface::class);
+ } catch (Throwable $e) {
+ return '';
+ }
+
+ $extraProperties = $extraPropertyValueProvider->getExtraProperties(
+ 'customer',
+ 'id_customer',
+ (int) $customer->id,
+ (int) $this->context->language->id,
+ (int) $this->context->shop->id,
+ true,
+ true
+ );
+
+ $moduleExtras = $extraProperties[$this->name] ?? [];
+ if (!is_array($moduleExtras) && !$moduleExtras instanceof ArrayAccess) {
+ $moduleExtras = [];
+ }
+
+ $this->context->smarty->assign([
+ 'demoExtraFieldTitle' => $this->trans('Extra fields (demoextrafield)', [], self::TRANSLATION_DOMAIN),
+ 'entityLabel' => $this->trans('Entity', [], self::TRANSLATION_DOMAIN),
+ 'entityName' => 'customer',
+ 'moduleExtras' => $moduleExtras,
+ ]);
+
+ return $this->display(__FILE__, 'views/templates/hook/customer_account_top.tpl');
+ }
+
+ /**
+ * Declares translation wordings so BO extraction can index them.
+ */
+ protected function registerTranslationWordings(): void
+ {
+ $domain = self::TRANSLATION_DOMAIN;
+
+ // Product
+ $this->trans('Dangerous product', [], $domain);
+ $this->trans('Indicates whether the product is dangerous', [], $domain);
+ $this->trans('Video link', [], $domain);
+ $this->trans('Video URL per language', [], $domain);
+ $this->trans('Custom date', [], $domain);
+ $this->trans('Custom date per shop', [], $domain);
+
+ // Category
+ $this->trans('Theme color', [], $domain);
+ $this->trans('Color associated with the category', [], $domain);
+ $this->trans('Marketing note', [], $domain);
+ $this->trans('Free note displayed in BO, API and FO', [], $domain);
+ $this->trans('Default supplier', [], $domain);
+ $this->trans('Select a PrestaShop supplier', [], $domain);
+
+ // Customer
+ $this->trans('Credit limit', [], $domain);
+ $this->trans('Maximum customer credit amount', [], $domain);
+ $this->trans('Metadata JSON', [], $domain);
+ $this->trans('Free JSON for customer metadata', [], $domain);
+
+ // Front templates
+ $this->trans('Extra fields (demoextrafield)', [], $domain);
+ $this->trans('Entity', [], $domain);
+ $this->trans('No extra fields found for this module.', [], $domain);
+ }
+}
+
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 @@
+
+
+
+
+
+ Dangerous product
+ Produit dangereux
+
+
+ Indicates whether the product is dangerous
+ Indique si le produit est dangereux
+
+
+ Video link
+ Lien vidéo
+
+
+ Video URL per language
+ URL de vidéo par langue
+
+
+ Custom date
+ Date personnalisée
+
+
+ Custom date per shop
+ Date personnalisée par boutique
+
+
+
+ Theme color
+ Couleur de thème
+
+
+ Color associated with the category
+ Couleur associée à la catégorie
+
+
+ Marketing note
+ Note marketing
+
+
+ Free note displayed in BO, API and FO
+ Note libre affichée dans le BO, l’API et le FO
+
+
+ Default supplier
+ Fournisseur par défaut
+
+
+ Select a PrestaShop supplier
+ Sélectionnez un fournisseur PrestaShop
+
+
+
+ Credit limit
+ Limite de crédit
+
+
+ Maximum customer credit amount
+ Montant maximum de crédit client
+
+
+ Metadata JSON
+ JSON de métadonnées
+
+
+ Free JSON for customer metadata
+ JSON libre pour les métadonnées client
+
+
+
+ Extra fields (demoextrafield)
+ Champs extra (demoextrafield)
+
+
+ Entity
+ Entité
+
+
+ No extra fields found for this module.
+ Aucun champ extra trouvé pour ce module.
+
+
+
+
+
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..e70ea34
--- /dev/null
+++ b/demoextrafield/views/templates/hook/cart_extra_product_info.tpl
@@ -0,0 +1,17 @@
+
+
diff --git a/demoextrafield/views/templates/hook/category_header.tpl b/demoextrafield/views/templates/hook/category_header.tpl
new file mode 100644
index 0000000..9f53c18
--- /dev/null
+++ b/demoextrafield/views/templates/hook/category_header.tpl
@@ -0,0 +1,18 @@
+
+
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..f0f6e4e
--- /dev/null
+++ b/demoextrafield/views/templates/hook/customer_account_top.tpl
@@ -0,0 +1,18 @@
+
+
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..f1cec75
--- /dev/null
+++ b/demoextrafield/views/templates/hook/product_additional_info.tpl
@@ -0,0 +1,18 @@
+
+