diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 9158b79..e3fac6b 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -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( diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index b80fde7..0e01f88 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -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 = ` -
-
- -
-
${ - labels.draft_already_exists || "Draft Already Exists" - }
- -
- - -
-

${ - labels.draft_exists_message || "A draft already exists" - }

-

${ - labels.draft_two_options || "You have two options:" - }

- - -
-
- ${ - labels.draft_option1_title || "Option 1" - } -
-
-

${labels.draft_option1_desc || "Merge with existing"}

-
    -
  • ${existing_items.length} ${ - labels.draft_items_count || "items" - } - ${labels.draft_existing_items || "Existing"}
  • -
  • ${current_items.length} ${ - labels.draft_items_count || "items" - } - ${labels.draft_current_items || "Current"}
  • -
-

- ${labels.draft_merge_note || "Products will be merged"} -

-
-
- - -
-
- ${ - labels.draft_option2_title || "Option 2" - } -
-
-

${labels.draft_option2_desc || "Replace with current"}

-

- ${ - labels.draft_replace_warning || - "Old draft will be deleted" - } -

-
-
-
- - -
- - - -
-
-
- `; - - // 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); diff --git a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py index f992915..8eb4114 100644 --- a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py +++ b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py @@ -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")) diff --git a/website_sale_aplicoop/tests/test_save_order_endpoints.py b/website_sale_aplicoop/tests/test_save_order_endpoints.py index 4e975fe..7a559db 100644 --- a/website_sale_aplicoop/tests/test_save_order_endpoints.py +++ b/website_sale_aplicoop/tests/test_save_order_endpoints.py @@ -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.