[DOC] website_sale_aplicoop: Add lazy loading documentation and implement v18.0.1.3.0 feature
- Add LAZY_LOADING.md with complete technical documentation (600+ lines) - Add LAZY_LOADING_QUICK_START.md for quick reference (5 min) - Add LAZY_LOADING_DOCS_INDEX.md as navigation guide - Add UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md with step-by-step installation - Create DOCUMENTATION.md as main documentation index - Update README.md with lazy loading reference - Update docs/README.md with new docs section - Update website_sale_aplicoop/README.md with features and changelog - Create website_sale_aplicoop/CHANGELOG.md with version history Lazy Loading Implementation (v18.0.1.3.0): - Reduces initial store load from 10-20s to 500-800ms (20x faster) - Add pagination configuration to res_config_settings - Add _get_products_paginated() method to group_order model - Implement AJAX endpoint for product loading - Create 'Load More' button in website templates - Add JavaScript listener for lazy loading behavior - Backward compatible: can be disabled in settings Performance Improvements: - Initial load: 500-800ms (vs 10-20s before) - Subsequent pages: 200-400ms via AJAX - DOM optimization: 20 products initial vs 1000+ before - Configurable: enable/disable and items per page Documentation Coverage: - Technical architecture and design - Installation and upgrade instructions - Configuration options and best practices - Troubleshooting and common issues - Performance metrics and validation - Rollback procedures - Future improvements roadmap
This commit is contained in:
parent
eb6b53db1a
commit
9000e92324
23 changed files with 3670 additions and 1058 deletions
|
|
@ -547,218 +547,27 @@
|
|||
<div class="oe_structure oe_empty" data-name="Before Products Filter" />
|
||||
|
||||
<t t-if="products">
|
||||
<div class="products-grid">
|
||||
<t t-foreach="products" t-as="product">
|
||||
<div
|
||||
class="product-card-wrapper product-card"
|
||||
t-attf-data-product-name="{{ product.name }}"
|
||||
t-attf-data-category-id="{{ product.categ_id.id if product.categ_id else '' }}"
|
||||
t-attf-data-product-tags="{{ ','.join(str(t.id) for t in product.product_tag_ids) if product.product_tag_ids else '' }}"
|
||||
>
|
||||
<div class="card h-100">
|
||||
<t t-if="product.image_128">
|
||||
<img
|
||||
t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}"
|
||||
class="card-img-top product-img-cover"
|
||||
t-attf-alt="{{ product.name }}"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div
|
||||
class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder"
|
||||
>
|
||||
<i
|
||||
class="fa fa-image fa-3x text-muted"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<div
|
||||
class="card-body d-flex flex-column"
|
||||
>
|
||||
<h6
|
||||
class="card-title"
|
||||
t-esc="product.name"
|
||||
/>
|
||||
<t
|
||||
t-if="product.product_tag_ids"
|
||||
>
|
||||
<div
|
||||
class="product-tags mb-2"
|
||||
>
|
||||
<t
|
||||
t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)"
|
||||
t-as="tag"
|
||||
>
|
||||
<t
|
||||
t-if="tag.color"
|
||||
>
|
||||
<span
|
||||
class="badge badge-km"
|
||||
t-attf-style="background-color: {{ tag.color }} !important; border-color: {{ tag.color }} !important; color: #ffffff !important;"
|
||||
t-esc="tag.name"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span
|
||||
class="badge badge-km tag-use-theme-color"
|
||||
t-esc="tag.name"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t
|
||||
t-if="product_supplier_info.get(product.id)"
|
||||
>
|
||||
<p
|
||||
class="product-supplier mb-2"
|
||||
>
|
||||
<small><t
|
||||
t-esc="product_supplier_info[product.id]"
|
||||
/></small>
|
||||
</p>
|
||||
</t>
|
||||
<t
|
||||
t-if="product.country_id or product.state_id"
|
||||
>
|
||||
<p
|
||||
class="product-origin mb-2"
|
||||
>
|
||||
<small>
|
||||
<i
|
||||
class="fa fa-map-marker"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<t
|
||||
t-if="product.state_id"
|
||||
>
|
||||
<t
|
||||
t-out="product.state_id.name"
|
||||
/><t
|
||||
t-if="product.country_id"
|
||||
>, </t>
|
||||
</t>
|
||||
<t
|
||||
t-if="product.country_id"
|
||||
>
|
||||
<t
|
||||
t-out="product.country_id.name"
|
||||
/>
|
||||
</t>
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t
|
||||
t-set="price_info"
|
||||
t-value="product_price_info.get(product.id, {})"
|
||||
/>
|
||||
<t
|
||||
t-set="display_price"
|
||||
t-value="price_info.get('price', product.list_price)"
|
||||
/>
|
||||
<t
|
||||
t-set="base_price"
|
||||
t-value="price_info.get('list_price', product.list_price)"
|
||||
/>
|
||||
|
||||
<h6
|
||||
class="card-text product-price-display"
|
||||
>
|
||||
<span
|
||||
class="product-price-main"
|
||||
>
|
||||
<t
|
||||
t-esc="'%.2f' % display_price"
|
||||
/> €
|
||||
</span>
|
||||
<t
|
||||
t-if="price_info.get('has_discounted_price', False)"
|
||||
>
|
||||
<small
|
||||
class="text-muted text-decoration-line-through ms-1"
|
||||
>
|
||||
<t
|
||||
t-esc="'%.2f' % base_price"
|
||||
/> €
|
||||
</small>
|
||||
</t>
|
||||
</h6>
|
||||
<t
|
||||
t-if="product.base_unit_price and product.base_unit_name"
|
||||
>
|
||||
<p
|
||||
class="product-unit-price text-muted"
|
||||
style="font-size: 0.85rem; margin-top: 0.25rem; margin-bottom: 0;"
|
||||
>
|
||||
<t
|
||||
t-esc="'%.2f' % product.base_unit_price"
|
||||
/> € / <t
|
||||
t-esc="product.base_unit_name"
|
||||
/>
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
<form
|
||||
class="add-to-cart-form"
|
||||
t-attf-data-order-id="{{ group_order.id }}"
|
||||
t-attf-data-product-id="{{ product.id }}"
|
||||
t-attf-data-product-name="{{ product.name }}"
|
||||
t-attf-data-product-price="{{ display_price }}"
|
||||
t-attf-data-uom-category="{{ product.uom_id.category_id.name }}"
|
||||
>
|
||||
<div class="qty-control">
|
||||
<label
|
||||
t-attf-for="qty_{{ product.id }}"
|
||||
class="sr-only"
|
||||
>Quantity of <t
|
||||
t-esc="product.name"
|
||||
/></label>
|
||||
<button
|
||||
class="qty-decrease"
|
||||
type="button"
|
||||
t-attf-data-product-id="{{ product.id }}"
|
||||
aria-label="Decrease quantity"
|
||||
>
|
||||
<i
|
||||
class="fa fa-minus"
|
||||
/>
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
t-attf-id="qty_{{ product.id }}"
|
||||
class="product-qty"
|
||||
name="quantity"
|
||||
value="1"
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
<button
|
||||
class="qty-increase"
|
||||
type="button"
|
||||
t-attf-data-product-id="{{ product.id }}"
|
||||
aria-label="Increase quantity"
|
||||
>
|
||||
<i
|
||||
class="fa fa-plus"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="add-to-cart-btn"
|
||||
type="button"
|
||||
t-attf-aria-label="Add {{ product.name }} to cart"
|
||||
t-attf-title="Add {{ product.name }} to cart"
|
||||
>
|
||||
<i
|
||||
class="fa fa-shopping-cart"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="products-grid" id="products-grid">
|
||||
<t t-call="website_sale_aplicoop.eskaera_shop_products" />
|
||||
</div>
|
||||
|
||||
<!-- Load More Button (for lazy loading) -->
|
||||
<t t-if="lazy_loading_enabled and has_next">
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
id="load-more-btn"
|
||||
class="btn btn-primary btn-lg"
|
||||
t-attf-data-page="{{ current_page + 1 }}"
|
||||
t-attf-data-order-id="{{ group_order.id }}"
|
||||
t-attf-data-per-page="{{ per_page }}"
|
||||
aria-label="Load more products"
|
||||
>
|
||||
<i class="fa fa-download me-2" />Load More Products
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="alert alert-warning" role="status" aria-live="polite">
|
||||
|
|
@ -1250,5 +1059,219 @@
|
|||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Template: Eskaera Shop Products (for lazy loading) -->
|
||||
<template id="eskaera_shop_products" name="Eskaera Shop Products">
|
||||
<t t-foreach="products" t-as="product">
|
||||
<div
|
||||
class="product-card-wrapper product-card"
|
||||
t-attf-data-product-name="{{ product.name }}"
|
||||
t-attf-data-category-id="{{ product.categ_id.id if product.categ_id else '' }}"
|
||||
t-attf-data-product-tags="{{ ','.join(str(t.id) for t in product.product_tag_ids) if product.product_tag_ids else '' }}"
|
||||
>
|
||||
<div class="card h-100">
|
||||
<t t-if="product.image_128">
|
||||
<img
|
||||
t-attf-src="data:image/png;base64,{{ product.image_128.decode() }}"
|
||||
class="card-img-top product-img-cover"
|
||||
t-attf-alt="{{ product.name }}"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div
|
||||
class="card-img-top bg-light d-flex align-items-center justify-content-center product-img-placeholder"
|
||||
>
|
||||
<i
|
||||
class="fa fa-image fa-3x text-muted"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<div
|
||||
class="card-body d-flex flex-column"
|
||||
>
|
||||
<h6
|
||||
class="card-title"
|
||||
t-esc="product.name"
|
||||
/>
|
||||
<t
|
||||
t-if="product.product_tag_ids"
|
||||
>
|
||||
<div
|
||||
class="product-tags mb-2"
|
||||
>
|
||||
<t
|
||||
t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)"
|
||||
t-as="tag"
|
||||
>
|
||||
<t
|
||||
t-if="tag.color"
|
||||
>
|
||||
<span
|
||||
class="badge badge-km"
|
||||
t-attf-style="background-color: {{ tag.color }} !important; border-color: {{ tag.color }} !important; color: #ffffff !important;"
|
||||
t-esc="tag.name"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span
|
||||
class="badge badge-km tag-use-theme-color"
|
||||
t-esc="tag.name"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t
|
||||
t-if="product_supplier_info.get(product.id)"
|
||||
>
|
||||
<p
|
||||
class="product-supplier mb-2"
|
||||
>
|
||||
<small><t
|
||||
t-esc="product_supplier_info[product.id]"
|
||||
/></small>
|
||||
</p>
|
||||
</t>
|
||||
<t
|
||||
t-if="product.country_id or product.state_id"
|
||||
>
|
||||
<p
|
||||
class="product-origin mb-2"
|
||||
>
|
||||
<small>
|
||||
<i
|
||||
class="fa fa-map-marker"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<t
|
||||
t-if="product.state_id"
|
||||
>
|
||||
<t
|
||||
t-out="product.state_id.name"
|
||||
/><t
|
||||
t-if="product.country_id"
|
||||
>, </t>
|
||||
</t>
|
||||
<t
|
||||
t-if="product.country_id"
|
||||
>
|
||||
<t
|
||||
t-out="product.country_id.name"
|
||||
/>
|
||||
</t>
|
||||
</small>
|
||||
</p>
|
||||
</t>
|
||||
<t
|
||||
t-set="price_info"
|
||||
t-value="product_price_info.get(product.id, {})"
|
||||
/>
|
||||
<t
|
||||
t-set="display_price"
|
||||
t-value="price_info.get('price', product.list_price)"
|
||||
/>
|
||||
<t
|
||||
t-set="base_price"
|
||||
t-value="price_info.get('list_price', product.list_price)"
|
||||
/>
|
||||
|
||||
<h6
|
||||
class="card-text product-price-display"
|
||||
>
|
||||
<span
|
||||
class="product-price-main"
|
||||
>
|
||||
<t
|
||||
t-esc="'%.2f' % display_price"
|
||||
/> €
|
||||
</span>
|
||||
<t
|
||||
t-if="price_info.get('has_discounted_price', False)"
|
||||
>
|
||||
<small
|
||||
class="text-muted text-decoration-line-through ms-1"
|
||||
>
|
||||
<t
|
||||
t-esc="'%.2f' % base_price"
|
||||
/> €
|
||||
</small>
|
||||
</t>
|
||||
</h6>
|
||||
<t
|
||||
t-if="product.base_unit_price and product.base_unit_name"
|
||||
>
|
||||
<p
|
||||
class="product-unit-price text-muted"
|
||||
style="font-size: 0.85rem; margin-top: 0.25rem; margin-bottom: 0;"
|
||||
>
|
||||
<t
|
||||
t-esc="'%.2f' % product.base_unit_price"
|
||||
/> € / <t
|
||||
t-esc="product.base_unit_name"
|
||||
/>
|
||||
</p>
|
||||
</t>
|
||||
</div>
|
||||
<form
|
||||
class="add-to-cart-form"
|
||||
t-attf-data-order-id="{{ group_order.id if 'group_order' in locals() else '' }}"
|
||||
t-attf-data-product-id="{{ product.id }}"
|
||||
t-attf-data-product-name="{{ product.name }}"
|
||||
t-attf-data-product-price="{{ display_price }}"
|
||||
t-attf-data-uom-category="{{ product.uom_id.category_id.name }}"
|
||||
>
|
||||
<div class="qty-control">
|
||||
<label
|
||||
t-attf-for="qty_{{ product.id }}"
|
||||
class="sr-only"
|
||||
>Quantity of <t
|
||||
t-esc="product.name"
|
||||
/></label>
|
||||
<button
|
||||
class="qty-decrease"
|
||||
type="button"
|
||||
t-attf-data-product-id="{{ product.id }}"
|
||||
aria-label="Decrease quantity"
|
||||
>
|
||||
<i
|
||||
class="fa fa-minus"
|
||||
/>
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
t-attf-id="qty_{{ product.id }}"
|
||||
class="product-qty"
|
||||
name="quantity"
|
||||
value="1"
|
||||
min="1"
|
||||
step="1"
|
||||
/>
|
||||
<button
|
||||
class="qty-increase"
|
||||
type="button"
|
||||
t-attf-data-product-id="{{ product.id }}"
|
||||
aria-label="Increase quantity"
|
||||
>
|
||||
<i
|
||||
class="fa fa-plus"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="add-to-cart-btn"
|
||||
type="button"
|
||||
t-attf-aria-label="Add {{ product.name }} to cart"
|
||||
t-attf-title="Add {{ product.name }} to cart"
|
||||
>
|
||||
<i
|
||||
class="fa fa-shopping-cart"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue