[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:
snt 2026-02-16 18:39:39 +01:00
parent eb6b53db1a
commit 9000e92324
23 changed files with 3670 additions and 1058 deletions

View file

@ -23,6 +23,37 @@
</div>
</div>
</div>
<h2>Shop Performance</h2>
<div class="row mt16 o_settings_container" id="eskaera_shop_settings">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="eskaera_lazy_loading_enabled" string="Enable Lazy Loading"/>
<div class="text-muted">
Load products in pages instead of all at once
</div>
<div class="content-group">
<div class="mt16">
<field name="eskaera_lazy_loading_enabled" class="oe_inline"/>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="eskaera_products_per_page" string="Products Per Page"/>
<div class="text-muted">
Number of products to load on initial page
</div>
<div class="content-group">
<div class="mt16">
<field name="eskaera_products_per_page" class="oe_inline"/>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Extend stock.picking search view to add consumer group filters -->
<record id="view_picking_internal_search_extended" model="ir.ui.view">
<field name="name">stock.picking.search.extended</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
<!-- Add consumer group search fields -->
<field name="partner_id" position="after">
<field name="consumer_group_id" string="Consumer Group"/>
<field name="group_order_id" string="Group Order"/>
</field>
<!-- Add consumer group filters -->
<filter name="internal" position="after">
<separator/>
<filter string="Home Delivery" name="filter_home_delivery"
domain="[('home_delivery', '=', True)]"/>
</filter>
<!-- Add group-by options for consumer groups -->
<filter name="picking_type" position="after">
<filter string="Consumer Group" name="group_by_consumer_group"
domain="[]" context="{'group_by': 'consumer_group_id'}"/>
<filter string="Group Order" name="group_by_group_order"
domain="[]" context="{'group_by': 'group_order_id'}"/>
<filter string="Pickup Date" name="group_by_pickup_date"
domain="[]" context="{'group_by': 'pickup_date'}"/>
</filter>
</field>
</record>
<!-- Extend stock.picking tree view to add hidden columns -->
<record id="view_picking_tree_extended" model="ir.ui.view">
<field name="name">stock.picking.tree.extended</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<!-- Add consumer group and home delivery fields as optional columns -->
<field name="partner_id" position="after">
<field name="consumer_group_id" string="Consumer Group"
optional="hide"/>
<field name="group_order_id" string="Group Order"
optional="hide"/>
<field name="pickup_date" string="Pickup Date"
optional="hide"/>
<field name="home_delivery" string="Home Delivery"
optional="hide" widget="boolean_toggle"/>
</field>
</field>
</record>
<!-- Extend stock.picking form view to show consumer group info -->
<record id="view_picking_form_extended" model="ir.ui.view">
<field name="name">stock.picking.form.extended</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<!-- Add consumer group info in header after partner -->
<field name="partner_id" position="after">
<field name="consumer_group_id"
invisible="not consumer_group_id"
readonly="1"/>
<field name="group_order_id"
invisible="not group_order_id"
readonly="1"/>
</field>
<!-- Add home delivery and pickup date in notebook page -->
<xpath expr="//page[@name='note']" position="after">
<page string="Consumer Group Info"
name="consumer_group_info"
invisible="not group_order_id">
<group>
<group>
<field name="home_delivery" readonly="1"/>
<field name="pickup_date" readonly="1"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View file

@ -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>