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} + +{/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 @@ +
+

{l s='Extra fields (demoextrafield)' d='Modules.Demoextrafield.Main'}

+ + {include file='./_extra_properties.tpl' objectModel=$product} +
+ 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 @@ +
+

{l s='Extra fields (demoextrafield)' d='Modules.Demoextrafield.Main'}

+ + {include file='./_extra_properties.tpl' objectModel=$category} +
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 @@ +
+

{l s='Extra fields (demoextrafield)' d='Modules.Demoextrafield.Main'}

+ + {* customerExtraData is built in hookDisplayCustomerAccountTop from ExtraPropertiesLazyArray. + internal_note has displayFront=false so it is absent from this output even though it exists in DB. *} + {include file='./_extra_properties.tpl' objectModel=$customerExtraData} +
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 @@ +
+

{l s='Extra fields (demoextrafield)' d='Modules.Demoextrafield.Main'}

+ + {* 1. Generic loop — iterates over all fields registered by this module. *} + {include file='./_extra_properties.tpl' objectModel=$product} + + {* 2. Named access — read a specific field directly without looping. + Useful when you need to act on a known field (conditional display, formatting, etc.). *} + {if $product.extra_properties.demoextrafield.is_dangerous|intval} +

+ {l s='⚠ This product is marked as dangerous.' d='Modules.Demoextrafield.Admin'} +

+ {/if} +
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 @@ +
+

{l s='Date last seen (extra field demo)' d='Modules.Demoextrafield.Admin'}

+ +