[FIX] website_sale_aplicoop: align pricing and drafts

This commit is contained in:
snt 2026-02-27 19:39:25 +01:00
parent aef57a3de4
commit a9c1f1f609
4 changed files with 258 additions and 533 deletions

View file

@ -6,6 +6,7 @@ import logging
from datetime import datetime
from datetime import timedelta
from odoo import fields
from odoo import http
from odoo.http import request
@ -302,7 +303,6 @@ class AplicoopWebsiteSale(WebsiteSale):
# Agregar a los hijos de su padre
category_map[parent_id]["children"].append(cat_info)
# Ordenar raíces y sus hijos por nombre
def sort_hierarchy(items):
items.sort(key=lambda x: x["name"])
for item in items:
@ -325,87 +325,49 @@ class AplicoopWebsiteSale(WebsiteSale):
Returns:
product.pricelist record or False if none found
"""
env = request.env
website = request.website
pricelist = None
# Try to get configured Aplicoop pricelist first
# 1) Configured pricelist from settings
try:
aplicoop_pricelist_id = (
request.env["ir.config_parameter"]
param_value = (
env["ir.config_parameter"]
.sudo()
.get_param("website_sale_aplicoop.pricelist_id")
)
if aplicoop_pricelist_id:
if param_value:
pricelist = (
request.env["product.pricelist"]
.sudo()
.browse(int(aplicoop_pricelist_id))
env["product.pricelist"].browse(int(param_value)).exists() or None
)
if pricelist.exists():
_logger.info(
"_resolve_pricelist: Using configured Aplicoop pricelist: %s (id=%s)",
pricelist.name,
pricelist.id,
)
return pricelist
else:
_logger.warning(
"_resolve_pricelist: Configured Aplicoop pricelist (id=%s) not found",
aplicoop_pricelist_id,
)
except Exception as err:
_logger.warning(
"_resolve_pricelist: Error getting Aplicoop pricelist: %s", str(err)
)
except Exception as e:
_logger.warning("_resolve_pricelist: error reading config param: %s", e)
# Fallback to website pricelist
try:
pricelist = request.website._get_current_pricelist()
if pricelist:
_logger.info(
"_resolve_pricelist: Using website pricelist: %s (id=%s)",
pricelist.name,
pricelist.id,
# 2) Website current pricelist
if not pricelist:
try:
pricelist = website._get_current_pricelist()
except Exception as e:
_logger.warning(
"_resolve_pricelist: fallback to website pricelist failed: %s", e
)
return pricelist
except Exception as err:
_logger.warning(
"_resolve_pricelist: Error getting website pricelist: %s", str(err)
)
# Final fallback to first active pricelist
pricelist = (
request.env["product.pricelist"]
.sudo()
.search([("active", "=", True)], limit=1)
)
if pricelist:
_logger.info(
"_resolve_pricelist: Using first active pricelist: %s (id=%s)",
pricelist.name,
pricelist.id,
)
return pricelist
# 3) First active pricelist as fallback
if not pricelist:
pricelist = env["product.pricelist"].sudo().search([], limit=1)
_logger.error(
"_resolve_pricelist: ERROR - No pricelist found! Pricing may fail."
)
return False
return pricelist
def _prepare_product_display_info(self, product, product_price_info):
"""Prepare all display information for a product in a QWeb-safe way.
This function pre-processes all values that might be None or require
conditional logic, so the template can use simple variable references
without complex expressions that confuse QWeb's parser.
"""Build display info for a product using precomputed prices.
Args:
product: product.template record
product_price_info: dict with 'price', 'list_price', etc.
product (product.product): Product variant.
product_price_info: dict with price data keyed by product.id.
Returns:
dict with all pre-processed display values ready for template
dict with display_price, safe_uom_category, quantity_step
"""
# Safety: Get price, ensure it's a float
price_data = product_price_info.get(product.id, {})
price = (
price_data.get("price", product.list_price)
@ -414,19 +376,14 @@ class AplicoopWebsiteSale(WebsiteSale):
)
price_safe = float(price) if price else 0.0
# Safety: Get UoM category name (use sudo for read-only display to avoid ACL issues)
uom_category_name = ""
quantity_step = 1 # Default step for integer quantities (Units)
quantity_step = 1
if product.uom_id:
uom = product.uom_id.sudo()
if uom.category_id:
uom_category_name = uom.category_id.sudo().name or ""
# Use XML IDs to detect fractional UoM categories (multilingual robust)
# This works regardless of translation/language
try:
# Get external ID for the UoM category
ir_model_data = request.env["ir.model.data"].sudo()
external_id = ir_model_data.search(
[
@ -437,18 +394,16 @@ class AplicoopWebsiteSale(WebsiteSale):
)
if external_id:
# Standard Odoo UoM categories requiring fractional step
fractional_categories = [
"uom.product_uom_categ_kgm", # Weight (kg, g, ton, etc.)
"uom.product_uom_categ_vol", # Volume (L, m³, etc.)
"uom.uom_categ_length", # Length/Distance (m, km, etc.)
"uom.uom_categ_surface", # Surface (m², ha, etc.)
"uom.product_uom_categ_kgm",
"uom.product_uom_categ_vol",
"uom.uom_categ_length",
"uom.uom_categ_surface",
]
full_xmlid = f"{external_id.module}.{external_id.name}"
if full_xmlid in fractional_categories:
quantity_step = 0.1
except Exception as e:
# Fallback to integer step on error
_logger.warning(
"_prepare_product_display_info: Error detecting UoM category XML ID for product %s: %s",
product.id,
@ -461,6 +416,70 @@ class AplicoopWebsiteSale(WebsiteSale):
"quantity_step": quantity_step,
}
def _get_pricing_info(self, product, pricelist, quantity=1.0, partner=None):
"""Compute pricing with taxes like website_sale but using a given pricelist.
Returns a dict with:
- price_unit: raw pricelist price (before taxes), suitable for sale.order.line
- price: display price (tax included/excluded per website setting)
- list_price: display list price (pre-discount) with same tax display
- has_discounted_price: bool
"""
try:
env = request.env
website = request.website
except RuntimeError:
env = product.env
website = env["website"].get_current_website()
partner = partner or env.user.partner_id
currency = pricelist.currency_id
company = website.company_id or product.company_id or env.company
price, rule_id = pricelist._get_product_price_rule(
product=product,
quantity=quantity,
target_currency=currency,
)
price_before_discount = price
pricelist_item = env["product.pricelist.item"].browse(rule_id)
if pricelist_item and pricelist_item._show_discount_on_shop():
price_before_discount = pricelist_item._compute_price_before_discount(
product=product,
quantity=quantity or 1.0,
date=fields.Date.context_today(pricelist),
uom=product.uom_id,
currency=currency,
)
has_discounted_price = price_before_discount > price
fiscal_position = website.fiscal_position_id.sudo()
product_taxes = product.sudo().taxes_id._filter_taxes_by_company(company)
taxes = (
fiscal_position.map_tax(product_taxes) if product_taxes else product_taxes
)
tax_display = "total_included"
def compute_display(amount):
if not taxes:
return amount
return taxes.compute_all(amount, currency, 1, product, partner)[tax_display]
display_price = compute_display(price)
display_list_price = compute_display(price_before_discount)
return {
"price_unit": price,
"price": display_price,
"list_price": display_list_price,
"has_discounted_price": has_discounted_price,
"discount": display_list_price - display_price,
"tax_included": tax_display == "total_included",
}
def _compute_price_info(self, products, pricelist):
"""Compute price info dict for a list of products using the given pricelist.
@ -473,23 +492,13 @@ class AplicoopWebsiteSale(WebsiteSale):
)
if product_variant and pricelist:
try:
price_info = product_variant._get_price(
qty=1.0,
pricelist=pricelist,
fposition=request.website.fiscal_position_id,
pricing = self._get_pricing_info(
product_variant,
pricelist,
quantity=1.0,
partner=request.env.user.partner_id,
)
price = price_info.get("value", 0.0)
original_price = price_info.get("original_value", 0.0)
discount = price_info.get("discount", 0.0)
has_discount = discount > 0
product_price_info[product.id] = {
"price": price,
"list_price": original_price,
"has_discounted_price": has_discount,
"discount": discount,
"tax_included": price_info.get("tax_included", True),
}
product_price_info[product.id] = pricing
except Exception as e:
_logger.warning(
"_compute_price_info: Error getting price for product %s (id=%s): %s. Using list_price fallback.",
@ -498,19 +507,23 @@ class AplicoopWebsiteSale(WebsiteSale):
str(e),
)
product_price_info[product.id] = {
"price_unit": product.list_price,
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": False,
"tax_included": request.website.show_line_subtotals_tax_selection
!= "tax_excluded",
}
else:
product_price_info[product.id] = {
"price_unit": product.list_price,
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": False,
"tax_included": request.website.show_line_subtotals_tax_selection
!= "tax_excluded",
}
return product_price_info
@ -814,7 +827,7 @@ class AplicoopWebsiteSale(WebsiteSale):
return order_id, group_order, current_user, items, is_delivery
def _process_cart_items(self, items, group_order):
def _process_cart_items(self, items, group_order, pricelist=None):
"""Process cart items and build sale.order line data.
Args:
@ -828,12 +841,13 @@ class AplicoopWebsiteSale(WebsiteSale):
ValueError: if no valid items after processing
"""
sale_order_lines = []
pricelist = pricelist or self._resolve_pricelist()
partner = request.env.user.partner_id
for item in items:
try:
product_id = int(item.get("product_id"))
quantity = float(item.get("quantity", 1))
price = float(item.get("product_price", 0))
product = request.env["product.product"].sudo().browse(product_id)
if not product.exists():
@ -846,10 +860,17 @@ class AplicoopWebsiteSale(WebsiteSale):
product_in_lang = product.with_context(lang=request.env.lang)
product_name = product_in_lang.name
pricing = self._get_pricing_info(
product,
pricelist,
quantity=quantity,
partner=partner,
)
line_data = {
"product_id": product_id,
"product_uom_qty": quantity,
"price_unit": price or product.list_price,
"price_unit": pricing.get("price_unit", product.list_price),
"name": product_name, # Force the translated product name
}
_logger.info("_process_cart_items: Adding line: %s", line_data)
@ -1018,19 +1039,7 @@ class AplicoopWebsiteSale(WebsiteSale):
return sale_order
def _build_confirmation_message(self, sale_order, group_order, is_delivery):
"""Build localized confirmation message for confirm_eskaera.
Translates message and pickup/delivery info according to user's language.
Handles day names and date formatting.
Args:
sale_order: sale.order record just created
group_order: group.order record
is_delivery: boolean indicating if home delivery
Returns:
dict with message, pickup_day, pickup_date, pickup_day_index
"""
"""Build localized confirmation message for confirm_eskaera."""
# Get pickup day index, localized name and date string using helper
pickup_day_name, pickup_date_str, pickup_day_index = self._format_pickup_info(
group_order, is_delivery
@ -1220,86 +1229,29 @@ class AplicoopWebsiteSale(WebsiteSale):
group_order,
current_user,
sale_order_lines,
merge_action,
existing_draft_id,
existing_drafts,
order_id,
):
"""Handle merge/replace logic for drafts and return (sale_order, merge_success).
"""Replace existing draft (if any) with new lines, else create it."""
existing_drafts: recordset of existing draft orders (may be empty)
"""
# Merge
if merge_action == "merge" and existing_draft_id:
existing_draft = (
request.env["sale.order"].sudo().browse(int(existing_draft_id))
)
if existing_draft.exists():
for new_line_data in sale_order_lines:
product_id = new_line_data[2]["product_id"]
new_quantity = new_line_data[2]["product_uom_qty"]
new_price = new_line_data[2]["price_unit"]
# Capture product_id as default arg to avoid late-binding in lambda (fix B023)
existing_line = existing_draft.order_line.filtered(
lambda line, pid=product_id: line.product_id.id == pid
)
if existing_line:
# Use sudo() to avoid permission issues with portal users
existing_line.sudo().write(
{
"product_uom_qty": existing_line.product_uom_qty
+ new_quantity
}
)
_logger.info(
"Merged item: product_id=%d, new total quantity=%.2f",
product_id,
existing_line.product_uom_qty,
)
else:
# Use sudo() to avoid permission issues with portal users
existing_draft.order_line.sudo().create(
{
"order_id": existing_draft.id,
"product_id": product_id,
"product_uom_qty": new_quantity,
"price_unit": new_price,
}
)
_logger.info(
"Added new item to draft: product_id=%d, quantity=%.2f",
product_id,
new_quantity,
)
return existing_draft, True
# Replace
if merge_action == "replace" and existing_draft_id and existing_drafts:
existing_drafts.unlink()
if existing_drafts:
draft = existing_drafts[0].sudo()
_logger.info(
"Deleted existing draft(s) for replace: %s",
existing_drafts.mapped("id"),
"Replacing existing draft order %s for partner %s",
draft.id,
current_user.partner_id.id,
)
order_vals = {
"partner_id": current_user.partner_id.id,
"order_line": sale_order_lines,
"state": "draft",
"group_order_id": order_id,
"pickup_day": group_order.pickup_day,
"pickup_date": group_order.pickup_date,
"home_delivery": group_order.home_delivery,
}
# Get salesperson for order creation (portal users need this)
salesperson = self._get_salesperson_for_order(current_user.partner_id)
if salesperson:
order_vals["user_id"] = salesperson.id
draft.write(
{
"order_line": [(5, 0, 0)] + sale_order_lines,
"group_order_id": order_id,
"pickup_day": group_order.pickup_day,
"pickup_date": group_order.pickup_date,
"home_delivery": group_order.home_delivery,
}
)
return draft
sale_order = request.env["sale.order"].sudo().create(order_vals)
return sale_order, False
# Default: create new draft
order_vals = {
"partner_id": current_user.partner_id.id,
"order_line": sale_order_lines,
@ -1315,7 +1267,7 @@ class AplicoopWebsiteSale(WebsiteSale):
order_vals["user_id"] = salesperson.id
sale_order = request.env["sale.order"].sudo().create(order_vals)
return sale_order, False
return sale_order
def _decode_json_body(self):
"""Safely decode JSON body from request. Returns dict or raises ValueError."""
@ -2045,7 +1997,9 @@ class AplicoopWebsiteSale(WebsiteSale):
# Build sale.order lines and create draft using helpers
try:
sale_order_lines = self._process_cart_items(items, group_order)
sale_order_lines = self._process_cart_items(
items, group_order, pricelist=self._resolve_pricelist()
)
except ValueError as e:
return request.make_response(
json.dumps({"error": str(e)}),
@ -2171,13 +2125,21 @@ class AplicoopWebsiteSale(WebsiteSale):
# Extract items from the draft order
items = []
pricelist = self._resolve_pricelist()
partner = current_user.partner_id
for line in draft_order.order_line:
pricing = self._get_pricing_info(
line.product_id,
pricelist,
quantity=line.product_uom_qty,
partner=partner,
)
items.append(
{
"product_id": line.product_id.id,
"product_name": line.product_id.name,
"quantity": line.product_uom_qty,
"product_price": line.price_unit,
"product_price": pricing.get("price", line.price_unit),
}
)
@ -2306,8 +2268,6 @@ class AplicoopWebsiteSale(WebsiteSale):
# Get cart items
items = data.get("items", [])
merge_action = data.get("merge_action") # 'merge' or 'replace'
existing_draft_id = data.get("existing_draft_id") # ID if replacing
if not items:
_logger.warning(
@ -2334,35 +2294,6 @@ class AplicoopWebsiteSale(WebsiteSale):
)
)
# If draft exists and no action specified, return the existing draft info
if existing_drafts and not merge_action:
existing_draft = existing_drafts[0] # Get first draft
existing_items = [
{
"product_id": line.product_id.id,
"product_name": line.product_id.name,
"quantity": line.product_uom_qty,
"product_price": line.price_unit,
}
for line in existing_draft.order_line
]
return request.make_response(
json.dumps(
{
"success": False,
"existing_draft": True,
"existing_draft_id": existing_draft.id,
"existing_items": existing_items,
"current_items": items,
"message": request.env._(
"A draft already exists for this week."
),
}
),
[("Content-Type", "application/json")],
)
_logger.info(
"Creating draft sale.order with %d items for partner %d",
len(items),
@ -2371,7 +2302,9 @@ class AplicoopWebsiteSale(WebsiteSale):
# Create sales.order lines from items using shared helper
try:
sale_order_lines = self._process_cart_items(items, group_order)
sale_order_lines = self._process_cart_items(
items, group_order, pricelist=self._resolve_pricelist()
)
except ValueError as e:
return request.make_response(
json.dumps({"error": str(e)}),
@ -2380,12 +2313,10 @@ class AplicoopWebsiteSale(WebsiteSale):
)
# Delegate merge/replace/create logic to helper
sale_order, merge_success = self._merge_or_replace_draft(
sale_order = self._merge_or_replace_draft(
group_order,
current_user,
sale_order_lines,
merge_action,
existing_draft_id,
existing_drafts,
order_id,
)
@ -2404,13 +2335,8 @@ class AplicoopWebsiteSale(WebsiteSale):
json.dumps(
{
"success": True,
"message": (
request.env._("Merged with existing draft")
if merge_success
else request.env._("Order saved as draft")
),
"message": request.env._("Order saved as draft"),
"sale_order_id": sale_order.id,
"merged": merge_success,
}
),
[("Content-Type", "application/json")],
@ -2481,7 +2407,9 @@ class AplicoopWebsiteSale(WebsiteSale):
# Process cart items using helper
try:
sale_order_lines = self._process_cart_items(items, group_order)
sale_order_lines = self._process_cart_items(
items, group_order, pricelist=self._resolve_pricelist()
)
except ValueError as e:
_logger.warning("confirm_eskaera: Cart processing error: %s", str(e))
return request.make_response(

View file

@ -1313,6 +1313,7 @@
var orderData = {
order_id: this.orderId,
items: items,
merge_action: "replace",
};
var xhr = new XMLHttpRequest();
@ -1332,9 +1333,6 @@
data.sale_order_id +
")";
self._showNotification("✓ " + successMsg, "success", 5000);
} else if (data.existing_draft) {
// A draft already exists - show modal with merge/replace options
self._showDraftConflictModal(data);
} else {
self._showNotification(
"Error: " + (data.error || labels.error_unknown || "Unknown error"),
@ -1536,9 +1534,6 @@
labels.draft_saved ||
"Order saved as draft successfully";
self._showNotification("\u2713 " + successMsg, "success", 5000);
} else if (data.existing_draft) {
// A draft already exists - show modal with merge/replace options
self._showDraftConflictModal(data);
} else {
self._showNotification(
"Error: " + (data.error || labels.error_unknown || "Unknown error"),
@ -1576,306 +1571,6 @@
xhr.send(JSON.stringify(orderData));
},
_showDraftConflictModal: function (data) {
/**
* Show modal with merge/replace options for existing draft.
* Uses labels from window.groupOrderShop.labels or falls back to defaults.
*/
var self = this;
// Get labels - they should already be loaded by page init
var labels =
window.groupOrderShop && window.groupOrderShop.labels
? window.groupOrderShop.labels
: self._getDefaultLabels();
console.log(
"[_showDraftConflictModal] Using labels:",
Object.keys(labels).length,
"keys available"
);
var existing_items = data.existing_items || [];
var current_items = data.current_items || [];
var existing_draft_id = data.existing_draft_id;
// Create modal HTML with inline styles (no Bootstrap needed)
var modalHTML = `
<div id="draftConflictModal" style="
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
">
<div style="
background: white;
border-radius: 8px;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
">
<!-- Header -->
<div style="
padding: 20px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
">
<h5 style="margin: 0; font-size: 1.25rem;">${
labels.draft_already_exists || "Draft Already Exists"
}</h5>
<button class="draft-modal-close" style="
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
">×</button>
</div>
<!-- Body -->
<div style="padding: 20px;">
<p><strong>${
labels.draft_exists_message || "A draft already exists"
}</strong></p>
<p style="margin-top: 15px;">${
labels.draft_two_options || "You have two options:"
}</p>
<!-- Option 1 -->
<div style="
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
overflow: hidden;
">
<div style="
background: #17a2b8;
color: white;
padding: 10px 15px;
font-weight: bold;
">
<i class="fa fa-code-fork"></i> ${
labels.draft_option1_title || "Option 1"
}
</div>
<div style="padding: 15px;">
<p>${labels.draft_option1_desc || "Merge with existing"}</p>
<ul style="margin: 10px 0; padding-left: 20px; font-size: 0.9rem;">
<li>${existing_items.length} ${
labels.draft_items_count || "items"
} - ${labels.draft_existing_items || "Existing"}</li>
<li>${current_items.length} ${
labels.draft_items_count || "items"
} - ${labels.draft_current_items || "Current"}</li>
</ul>
<p style="color: #666; font-size: 0.85rem; margin: 10px 0 0 0;">
${labels.draft_merge_note || "Products will be merged"}
</p>
</div>
</div>
<!-- Option 2 -->
<div style="
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
">
<div style="
background: #ffc107;
color: black;
padding: 10px 15px;
font-weight: bold;
">
<i class="fa fa-refresh"></i> ${
labels.draft_option2_title || "Option 2"
}
</div>
<div style="padding: 15px;">
<p>${labels.draft_option2_desc || "Replace with current"}</p>
<p style="color: #666; font-size: 0.85rem; margin: 0;">
<i class="fa fa-exclamation-triangle"></i> ${
labels.draft_replace_warning ||
"Old draft will be deleted"
}
</p>
</div>
</div>
</div>
<!-- Footer -->
<div style="
padding: 15px 20px;
border-top: 1px solid #ddd;
display: flex;
gap: 10px;
justify-content: flex-end;
">
<button class="draft-modal-close" style="
padding: 8px 16px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
">${labels.cancel || "Cancel"}</button>
<button id="mergeBtn" data-existing-id="${existing_draft_id}" style="
padding: 8px 16px;
background: #17a2b8;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
">
<i class="fa fa-code-fork"></i> ${labels.draft_merge_btn || "Merge"}
</button>
<button id="replaceBtn" data-existing-id="${existing_draft_id}" style="
padding: 8px 16px;
background: #ffc107;
color: black;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
">
<i class="fa fa-refresh"></i> ${
labels.draft_replace_btn || "Replace"
}
</button>
</div>
</div>
</div>
`;
// Remove existing modal if any
var existingModal = document.getElementById("draftConflictModal");
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML("beforeend", modalHTML);
var modalElement = document.getElementById("draftConflictModal");
// Handle close buttons
document.querySelectorAll(".draft-modal-close").forEach(function (btn) {
btn.addEventListener("click", function () {
modalElement.remove();
});
});
// Handle merge button
document.getElementById("mergeBtn").addEventListener("click", function () {
var existingId = this.getAttribute("data-existing-id");
modalElement.remove();
self._executeSaveDraftWithAction("merge", existingId);
});
// Handle replace button
document.getElementById("replaceBtn").addEventListener("click", function () {
var existingId = this.getAttribute("data-existing-id");
modalElement.remove();
self._executeSaveDraftWithAction("replace", existingId);
});
// Close modal when clicking outside
modalElement.addEventListener("click", function (e) {
if (e.target === modalElement) {
modalElement.remove();
}
});
},
_executeSaveDraftWithAction: function (action, existingDraftId) {
/**
* Execute save draft with merge or replace action.
*/
var self = this;
var items = [];
Object.keys(this.cart).forEach(function (productId) {
var item = self.cart[productId];
items.push({
product_id: productId,
product_name: item.name,
quantity: item.qty,
product_price: item.price,
});
});
var orderData = {
order_id: this.orderId,
items: items,
merge_action: action,
existing_draft_id: existingDraftId,
};
var xhr = new XMLHttpRequest();
xhr.open("POST", "/eskaera/save-order", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
console.log("Response:", data);
if (data.success) {
// Use server-provided labels instead of hardcoding
var labels = self._getLabels();
// Use the translated messages from server
var msg = data.merged
? "✓ " +
(labels.draft_merged_success || "Draft merged successfully")
: "✓ " +
(labels.draft_replaced_success || "Draft replaced successfully");
self._showNotification(msg, "success", 5000);
} else {
var labels = self._getLabels();
self._showNotification(
"Error: " + (data.error || labels.error_unknown || "Unknown error"),
"danger"
);
}
} catch (e) {
console.error("Error parsing response:", e);
self._showNotification("Error processing response", "danger");
}
} else {
try {
var errorData = JSON.parse(xhr.responseText);
self._showNotification(
"Error " + xhr.status + ": " + (errorData.error || "Request error"),
"danger"
);
} catch (e) {
self._showNotification(
"Error saving order (HTTP " + xhr.status + ")",
"danger"
);
}
}
};
xhr.onerror = function () {
self._showNotification("Connection error", "danger");
};
xhr.send(JSON.stringify(orderData));
},
_confirmOrder: function () {
console.log("=== _confirmOrder started ===");
console.log("orderId:", this.orderId);

View file

@ -16,6 +16,8 @@ Coverage:
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from ..controllers.website_sale import AplicoopWebsiteSale
@tagged("post_install", "-at_install")
class TestPricingWithPricelist(TransactionCase):
@ -49,6 +51,7 @@ class TestPricingWithPricelist(TransactionCase):
"company_ids": [(6, 0, [self.company.id])],
}
)
self.partner = self.user.partner_id
# Get or create default tax group
tax_group = self.env["account.tax.group"].search(
@ -161,6 +164,9 @@ class TestPricingWithPricelist(TransactionCase):
}
)
# Controller helper
self.controller = AplicoopWebsiteSale()
# Create group order
self.group_order = self.env["group.order"].create(
{
@ -493,3 +499,52 @@ class TestPricingWithPricelist(TransactionCase):
except Exception:
# If it raises, that's also acceptable behavior
self.assertTrue(True, "Negative quantity properly rejected")
def test_pricing_helper_uses_config_pricelist_and_taxes(self):
"""Pricing helper must apply configured pricelist and include taxes for display."""
website = self.env.ref("website.default_website").sudo()
website.write(
{
"show_line_subtotals_tax_selection": "tax_included",
"company_id": self.company.id,
}
)
pricelist_discount = self.env["product.pricelist"].create(
{
"name": "Config Pricelist 10%",
"company_id": self.company.id,
"item_ids": [
(
0,
0,
{
"compute_price": "percentage",
"percent_price": 10.0,
"applied_on": "3_global",
},
)
],
}
)
self.env["ir.config_parameter"].sudo().set_param(
"website_sale_aplicoop.pricelist_id", pricelist_discount.id
)
product = self.product_with_tax # 100€ + 21%
pricing = self.controller._get_pricing_info(
product,
pricelist_discount,
quantity=1.0,
partner=self.partner,
)
# price_unit uses pricelist (10% discount)
self.assertAlmostEqual(pricing["price_unit"], 90.0, places=2)
# display price must include taxes (21% on discounted price)
self.assertAlmostEqual(pricing["price"], 108.9, places=2)
self.assertTrue(pricing.get("tax_included"))
self.assertTrue(pricing.get("has_discounted_price"))

View file

@ -16,6 +16,8 @@ from datetime import timedelta
from odoo.tests.common import TransactionCase
from ..controllers.website_sale import AplicoopWebsiteSale
class TestSaveOrderEndpoints(TransactionCase):
"""Test suite for order-saving endpoints."""
@ -94,6 +96,51 @@ class TestSaveOrderEndpoints(TransactionCase):
# Associate product with group order
self.group_order.product_ids = [(4, self.product.id)]
# Helper: controller instance for pure helpers
self.controller = AplicoopWebsiteSale()
def _build_line(self, product, qty, price):
return (
0,
0,
{"product_id": product.id, "product_uom_qty": qty, "price_unit": price},
)
def test_merge_or_replace_replaces_existing_draft(self):
"""Existing draft must be replaced (not merged) with new lines."""
# Existing draft with one line
existing = self.env["sale.order"].create(
{
"partner_id": self.member_partner.id,
"group_order_id": self.group_order.id,
"pickup_day": self.group_order.pickup_day,
"pickup_date": self.group_order.pickup_date,
"home_delivery": self.group_order.home_delivery,
"order_line": [self._build_line(self.product, 1, 10.0)],
"state": "draft",
}
)
new_lines = [self._build_line(self.product, 3, 99.0)]
result = self.controller._merge_or_replace_draft(
self.group_order,
self.user,
new_lines,
existing,
self.group_order.id,
)
self.assertEqual(result.id, existing.id, "Should reuse existing draft")
self.assertEqual(len(result.order_line), 1, "Only one line should remain")
self.assertEqual(
result.order_line[0].product_uom_qty, 3, "Quantity must be replaced"
)
self.assertEqual(
result.order_line[0].price_unit, 99.0, "Price must be replaced"
)
def test_save_eskaera_draft_creates_order_with_group_order_id(self):
"""
Test that save_eskaera_draft() creates a sale.order with group_order_id.