[IMP] website_sale_aplicoop: filter and cap stock using forecasted net qty
Group orders are not confirmed until the cutoff date, so draft/sent sale.order lines never generate stock.moves and are invisible to virtual_available. This change makes the shop aware of that demand. - group.order._compute_draft_sale_demand: queries sale.order.line in draft/sent state (mirroring sale_stock forecasted report logic) and returns pending demand per product.id in the product's own UoM. - _get_products_for_group_order: delegates to new _apply_stock_filter_and_sort which excludes storable products whose forecasted net qty (virtual_available − draft demand) <= 0, unless allow_out_of_stock_order. - _compute_stock_ribbons: reads draft_demand_by_product from ORM context so is_out_of_stock / is_low_stock / dynamic_ribbon_id reflect net qty. - Controller: new _prepare_draft_stock_data helper calculates demand once per request, injects context, and builds product_max_qty dict. Applied in eskaera_shop, load_eskaera_page and load_products_ajax. - Template: qty input gets max and data-max-qty from product_max_qty. - JS: blocks add-to-cart if requested quantity exceeds data-max-qty. - Fixes type check: type=='consu' → is_storable=True (Odoo 18 semantics). - 21 new tests in test_forecasted_stock.py covering demand calculation, ribbon logic with context, and group order filtering. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b484e3dc2e
commit
1d6747e703
7 changed files with 453 additions and 9 deletions
|
|
@ -572,6 +572,25 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"""Delegate preparation of product maps to products helper."""
|
||||
return _products._prepare_products_maps(self, products, pricelist)
|
||||
|
||||
def _prepare_draft_stock_data(self, products):
|
||||
"""Return (products_with_context, product_max_qty) for Eskaera stock rendering.
|
||||
|
||||
Computes draft/sent sale.order demand and injects it as context so that
|
||||
is_out_of_stock / is_low_stock / dynamic_ribbon_id reflect forecasted net qty.
|
||||
product_max_qty maps product.id → net qty for storable products where
|
||||
allow_out_of_stock_order is False and net qty > 0 (used as input[max]).
|
||||
"""
|
||||
draft_demand = request.env["group.order"]._compute_draft_sale_demand(products)
|
||||
products_ctx = products.with_context(draft_demand_by_product=draft_demand)
|
||||
product_max_qty = {}
|
||||
for p in products_ctx:
|
||||
allow_oos = getattr(p, "allow_out_of_stock_order", True)
|
||||
if not allow_oos and getattr(p.product_tmpl_id, "is_storable", False):
|
||||
net = p.virtual_available - draft_demand.get(p.id, 0.0)
|
||||
if net > 0:
|
||||
product_max_qty[p.id] = net
|
||||
return products_ctx, product_max_qty
|
||||
|
||||
def _merge_or_replace_draft(
|
||||
self,
|
||||
group_order,
|
||||
|
|
@ -790,6 +809,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
filtered_products_dict,
|
||||
) = self._prepare_products_maps(products, pricelist)
|
||||
|
||||
# Inject draft sale demand context so ribbons/is_out_of_stock use forecasted net qty
|
||||
products, product_max_qty = self._prepare_draft_stock_data(products)
|
||||
|
||||
# Manage session for separate cart per order
|
||||
session_key = f"eskaera_{order_id}"
|
||||
cart = request.session.get(session_key, {})
|
||||
|
|
@ -838,6 +860,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"has_next": has_next,
|
||||
"total_products": total_products,
|
||||
"module_version": self._get_installed_module_version(),
|
||||
"product_max_qty": product_max_qty,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -931,6 +954,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
product, product_price_info
|
||||
)
|
||||
|
||||
# Inject draft demand context for ribbons/stock flags
|
||||
products_page, product_max_qty = self._prepare_draft_stock_data(products_page)
|
||||
|
||||
labels = self.get_checkout_labels()
|
||||
|
||||
return request.render(
|
||||
|
|
@ -945,6 +971,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"labels": labels,
|
||||
"has_next": has_next,
|
||||
"next_page": page + 1,
|
||||
"product_max_qty": product_max_qty,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1047,6 +1074,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
for product in products_page
|
||||
}
|
||||
|
||||
# Inject draft demand context for ribbons/stock flags
|
||||
products_page, product_max_qty = self._prepare_draft_stock_data(products_page)
|
||||
|
||||
# Render HTML
|
||||
html = (
|
||||
request.env["ir.ui.view"]
|
||||
|
|
@ -1063,6 +1093,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"labels": self.get_checkout_labels(),
|
||||
"has_next": has_next,
|
||||
"next_page": page + 1,
|
||||
"product_max_qty": product_max_qty,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue