diff --git a/stock_picking_batch_custom/models/__init__.py b/stock_picking_batch_custom/models/__init__.py index c566790..24709d5 100644 --- a/stock_picking_batch_custom/models/__init__.py +++ b/stock_picking_batch_custom/models/__init__.py @@ -1,4 +1,2 @@ from . import stock_move_line # noqa: F401 -from . import stock_backorder_confirmation # noqa: F401 -from . import stock_picking # noqa: F401 from . import stock_picking_batch # noqa: F401 diff --git a/stock_picking_batch_custom/models/stock_backorder_confirmation.py b/stock_picking_batch_custom/models/stock_backorder_confirmation.py deleted file mode 100644 index ab246c5..0000000 --- a/stock_picking_batch_custom/models/stock_backorder_confirmation.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import models - - -class StockBackorderConfirmation(models.TransientModel): - _inherit = "stock.backorder.confirmation" - - def _check_batch_summary_lines(self): - pickings = self.env["stock.picking"].browse( - self.env.context.get("button_validate_picking_ids", []) - ) - for batch in pickings.batch_id: - batch._check_all_products_collected() - - def process(self): - self._check_batch_summary_lines() - return super().process() - - def process_cancel_backorder(self): - self._check_batch_summary_lines() - return super().process_cancel_backorder() diff --git a/stock_picking_batch_custom/models/stock_move_line.py b/stock_picking_batch_custom/models/stock_move_line.py index a388884..82bd6d2 100644 --- a/stock_picking_batch_custom/models/stock_move_line.py +++ b/stock_picking_batch_custom/models/stock_move_line.py @@ -1,7 +1,6 @@ # Copyright 2026 Criptomart # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api from odoo import fields from odoo import models @@ -22,18 +21,3 @@ class StockMoveLine(models.Model): default=False, copy=False, ) - - consumer_group_id = fields.Many2one( - comodel_name="res.partner", - compute="_compute_consumer_group_id", - readonly=True, - ) - - @api.depends("picking_id") - def _compute_consumer_group_id(self): - for line in self: - picking = line.picking_id - if picking: - line.consumer_group_id = picking.batch_consumer_group_id - else: - line.consumer_group_id = False diff --git a/stock_picking_batch_custom/models/stock_picking.py b/stock_picking_batch_custom/models/stock_picking.py deleted file mode 100644 index f4dc0a2..0000000 --- a/stock_picking_batch_custom/models/stock_picking.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import fields -from odoo import models - - -class StockPicking(models.Model): - _inherit = "stock.picking" - - batch_consumer_group_id = fields.Many2one( - comodel_name="res.partner", - compute="_compute_batch_consumer_group_id", - readonly=True, - string="Batch Consumer Group", - ) - - def _compute_batch_consumer_group_id(self): - for picking in self: - sale = picking.sale_id if "sale_id" in picking._fields else False - if sale and "consumer_group_id" in sale._fields: - picking.batch_consumer_group_id = sale.consumer_group_id - else: - picking.batch_consumer_group_id = False - - def _pre_action_done_hook(self): - """Run collected checks only after Odoo resolves backorders. - - This keeps the product summary checkbox informative during the initial - batch validation click, but still enforces it once the user confirms the - actual picking flow that will be processed. - """ - - res = super()._pre_action_done_hook() - if res is not True: - return res - - self._check_batch_collected_products() - return res - - def _check_batch_collected_products(self): - for batch in self.batch_id: - batch_pickings = self.filtered( - lambda picking, batch=batch: picking.batch_id == batch - ) - processed_product_ids = ( - batch_pickings.move_line_ids.filtered( - lambda line: line.move_id.state not in ("cancel", "done") - and line.product_id - and line.quantity - ) - .mapped("product_id") - .ids - ) - if not processed_product_ids: - continue - batch._check_all_products_collected(processed_product_ids) diff --git a/stock_picking_batch_custom/models/stock_picking_batch.py b/stock_picking_batch_custom/models/stock_picking_batch.py index 918c92b..798678c 100644 --- a/stock_picking_batch_custom/models/stock_picking_batch.py +++ b/stock_picking_batch_custom/models/stock_picking_batch.py @@ -140,20 +140,11 @@ class StockPickingBatch(models.Model): else: batch.summary_line_ids = [fields.Command.clear()] - def _check_all_products_collected(self, product_ids=None): - """Ensure collected is checked for processed products only. - - The product summary remains informative until Odoo knows which products - are actually being validated after the backorder decision. - """ - + def _check_all_products_collected(self): + """Ensure all product summary lines are marked as collected before done.""" for batch in self: not_collected_lines = batch.summary_line_ids.filtered( - lambda line: ( - line.qty_done > 0 - and not line.is_collected - and (not product_ids or line.product_id.id in product_ids) - ) + lambda line: not line.is_collected ) if not not_collected_lines: continue @@ -170,6 +161,10 @@ class StockPickingBatch(models.Model): ) raise UserError(message) + def action_done(self): + self._check_all_products_collected() + return super().action_done() + class StockPickingBatchSummaryLine(models.Model): _name = "stock.picking.batch.summary.line" diff --git a/stock_picking_batch_custom/tests/test_batch_summary.py b/stock_picking_batch_custom/tests/test_batch_summary.py index 2d96b48..07f2518 100644 --- a/stock_picking_batch_custom/tests/test_batch_summary.py +++ b/stock_picking_batch_custom/tests/test_batch_summary.py @@ -1,11 +1,9 @@ # Copyright 2026 Criptomart # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from unittest.mock import patch - from odoo.exceptions import UserError -from odoo.tests import TransactionCase from odoo.tests import tagged +from odoo.tests.common import TransactionCase @tagged("-at_install", "post_install") @@ -26,13 +24,6 @@ class TestBatchSummary(TransactionCase): "uom_po_id": cls.uom_unit.id, } ) - cls.product_2 = cls.env["product.product"].create( - { - "name": "Test Product 2", - "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, - } - ) cls.batch = cls.env["stock.picking.batch"].create( {"name": "Batch Test", "picking_type_id": cls.picking_type.id} @@ -122,60 +113,6 @@ class TestBatchSummary(TransactionCase): picking.batch_id = batch.id return batch - def _create_partial_batch(self, qty_demanded=2.0, qty_done=1.0, extra_move=False): - batch = self.env["stock.picking.batch"].create( - {"name": "Batch Partial", "picking_type_id": self.picking_type.id} - ) - batch.picking_type_id.create_backorder = "ask" - picking = self.env["stock.picking"].create( - { - "picking_type_id": self.picking_type.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - "batch_id": batch.id, - } - ) - - move = self.env["stock.move"].create( - { - "name": self.product.name, - "product_id": self.product.id, - "product_uom_qty": qty_demanded, - "product_uom": self.uom_unit.id, - "picking_id": picking.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - self.env["stock.move.line"].create( - { - "move_id": move.id, - "picking_id": picking.id, - "product_id": self.product.id, - "product_uom_id": self.uom_unit.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - "quantity": qty_done, - } - ) - - if extra_move: - self.env["stock.move"].create( - { - "name": self.product_2.name, - "product_id": self.product_2.id, - "product_uom_qty": 1.0, - "product_uom": self.uom_unit.id, - "picking_id": picking.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - - batch.action_confirm() - batch._compute_summary_line_ids() - return batch - # Tests def test_totals_and_pending_with_conversion(self): """Totals aggregate per product with UoM conversion and pending.""" @@ -253,6 +190,14 @@ class TestBatchSummary(TransactionCase): self.assertFalse(batch.summary_line_ids) # 2. Create pickings with moves (without batch assignment) + product2 = self.env["product.product"].create( + { + "name": "Test Product 2", + "uom_id": self.uom_unit.id, + "uom_po_id": self.uom_unit.id, + } + ) + picking_a = self.env["stock.picking"].create( { "picking_type_id": self.picking_type.id, @@ -281,8 +226,8 @@ class TestBatchSummary(TransactionCase): ) self.env["stock.move"].create( { - "name": self.product_2.name, - "product_id": self.product_2.id, + "name": product2.name, + "product_id": product2.id, "product_uom_qty": 10.0, "product_uom": self.uom_unit.id, "picking_id": picking_b.id, @@ -331,7 +276,7 @@ class TestBatchSummary(TransactionCase): # Two products expected products_in_summary = batch.summary_line_ids.mapped("product_id") self.assertIn(self.product, products_in_summary) - self.assertIn(self.product_2, products_in_summary) + self.assertIn(product2, products_in_summary) # Check aggregated quantities line_product1 = batch.summary_line_ids.filtered( @@ -340,12 +285,12 @@ class TestBatchSummary(TransactionCase): self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3 line_product2 = batch.summary_line_ids.filtered( - lambda line: line.product_id == self.product_2 + lambda line: line.product_id == product2 ) self.assertAlmostEqual(line_product2.qty_demanded, 10.0) - def test_done_requires_all_summary_lines_collected_without_backorder(self): - """Full validation still blocks if processed products are unchecked.""" + def test_done_requires_all_summary_lines_collected(self): + """Batch validation must fail if there are unchecked collected lines.""" batch = self._create_batch_with_pickings() batch.action_confirm() @@ -357,32 +302,6 @@ class TestBatchSummary(TransactionCase): with self.assertRaises(UserError): batch.action_done() - def test_partial_validation_waits_for_backorder_wizard_before_blocking(self): - """Partial batch validation must open the backorder wizard first.""" - - batch = self._create_partial_batch() - - with patch.object( - type(self.env["stock.picking"]), - "_check_backorder", - autospec=True, - return_value=batch.picking_ids, - ): - action = batch.action_done() - - self.assertIsInstance(action, dict) - self.assertEqual(action.get("res_model"), "stock.backorder.confirmation") - - def test_partial_validation_checks_only_processed_products(self): - """Unchecked lines with no processed quantity must remain informative.""" - - batch = self._create_partial_batch(extra_move=True) - - with self.assertRaisesRegex(UserError, self.product.display_name) as err: - batch._check_all_products_collected([self.product.id]) - - self.assertNotIn(self.product_2.display_name, str(err.exception)) - def test_check_all_products_collected_passes_when_all_checked(self): """Collected validation helper must pass when all lines are checked.""" diff --git a/stock_picking_batch_custom/views/stock_move_line_views.xml b/stock_picking_batch_custom/views/stock_move_line_views.xml index 7fbc218..b857a2c 100644 --- a/stock_picking_batch_custom/views/stock_move_line_views.xml +++ b/stock_picking_batch_custom/views/stock_move_line_views.xml @@ -5,27 +5,11 @@ stock.move.line - - hide - - - hide - - - hide - - - hide - - - hide - - diff --git a/stock_picking_batch_custom/views/stock_picking_batch_views.xml b/stock_picking_batch_custom/views/stock_picking_batch_views.xml index 0c6b75e..43b3ae2 100644 --- a/stock_picking_batch_custom/views/stock_picking_batch_views.xml +++ b/stock_picking_batch_custom/views/stock_picking_batch_views.xml @@ -28,15 +28,4 @@ - - - stock.picking.batch.picking.tree.consumer.group - stock.picking - - - - - - - diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index ebdf223..e7c1503 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -768,8 +768,6 @@ class AplicoopWebsiteSale(WebsiteSale): if not current_user.partner_id: raise ValueError("User has no associated partner") from None - self._validate_user_group_access(group_order, current_user) - # Validate items items = data.get("items", []) if not items: @@ -822,8 +820,6 @@ class AplicoopWebsiteSale(WebsiteSale): if not current_user.partner_id: raise ValueError("User has no associated partner") - self._validate_user_group_access(group_order, current_user) - # Validate items items = data.get("items", []) if not items: @@ -891,8 +887,6 @@ class AplicoopWebsiteSale(WebsiteSale): if not current_user.partner_id: raise ValueError("User has no associated partner") - self._validate_user_group_access(group_order, current_user) - # Validate items items = data.get("items", []) if not items: @@ -927,29 +921,6 @@ class AplicoopWebsiteSale(WebsiteSale): return False return bool(value) - def _get_effective_delivery_context(self, group_order, is_delivery): - """Return effective home delivery flag and commitment date. - - Delivery is effective only when requested by the user and enabled - in the group order configuration. - """ - delivery_requested = self._to_bool(is_delivery) - delivery_enabled = bool(group_order and group_order.home_delivery) - effective_home_delivery = delivery_requested and delivery_enabled - - if delivery_requested and not delivery_enabled: - _logger.info( - "Delivery requested but disabled in group order %s; using pickup flow", - group_order.id if group_order else "N/A", - ) - - if effective_home_delivery: - commitment_date = group_order.delivery_date or group_order.pickup_date - else: - commitment_date = group_order.pickup_date if group_order else False - - return effective_home_delivery, commitment_date - def _process_cart_items(self, items, group_order, pricelist=None): """Process cart items and build sale.order line data. @@ -1038,35 +1009,6 @@ class AplicoopWebsiteSale(WebsiteSale): # No salesperson found return False - def _get_consumer_group_for_user(self, group_order, current_user): - """Return the matching consumer group for the user in this group order.""" - partner = current_user.partner_id - if not partner or not group_order: - return False - - user_group_ids = set(partner.group_ids.ids) - for consumer_group in group_order.group_ids: - if consumer_group.id in user_group_ids: - return consumer_group.id - - _logger.warning( - "_get_consumer_group_for_user: User %s (%s) is not member of any " - "consumer group in order %s. user_groups=%s, order_groups=%s", - current_user.name, - current_user.id, - group_order.id, - sorted(user_group_ids), - group_order.group_ids.ids, - ) - return False - - def _validate_user_group_access(self, group_order, current_user): - """Ensure the user belongs to at least one consumer group of the order.""" - consumer_group_id = self._get_consumer_group_for_user(group_order, current_user) - if not consumer_group_id: - raise ValueError("User is not a member of any consumer group in this order") - return consumer_group_id - def _create_or_update_sale_order( self, group_order, @@ -1080,7 +1022,11 @@ class AplicoopWebsiteSale(WebsiteSale): Returns the sale.order record. """ - consumer_group_id = self._validate_user_group_access(group_order, current_user) + # consumer_group_id comes directly from group_order (first/only group) + # No calculations - data propagates directly from the order context + consumer_group_id = ( + group_order.group_ids[0].id if group_order.group_ids else False + ) _logger.info( "[CONSUMER_GROUP DEBUG] _create_or_update_sale_order: " @@ -1090,25 +1036,22 @@ class AplicoopWebsiteSale(WebsiteSale): consumer_group_id, ) - effective_home_delivery, calculated_commitment_date = ( - self._get_effective_delivery_context(group_order, is_delivery) - ) - - # Explicit commitment_date wins; otherwise use calculated value + # commitment_date: use provided or calculate from group_order if not commitment_date: - commitment_date = calculated_commitment_date + commitment_date = ( + group_order.delivery_date if is_delivery else group_order.pickup_date + ) if existing_order: # Update existing order with new lines and propagate fields # Use sudo() to avoid permission issues with portal users existing_order_sudo = existing_order.sudo() - # (5,) clears all existing lines before adding the new ones - existing_order_sudo.order_line = [(5,)] + sale_order_lines + existing_order_sudo.order_line = sale_order_lines if not existing_order_sudo.group_order_id: existing_order_sudo.group_order_id = group_order.id existing_order_sudo.pickup_day = group_order.pickup_day existing_order_sudo.pickup_date = group_order.pickup_date - existing_order_sudo.home_delivery = effective_home_delivery + existing_order_sudo.home_delivery = is_delivery existing_order_sudo.consumer_group_id = consumer_group_id if commitment_date: existing_order_sudo.commitment_date = commitment_date @@ -1116,7 +1059,7 @@ class AplicoopWebsiteSale(WebsiteSale): "Updated existing sale.order %d: commitment_date=%s, home_delivery=%s, consumer_group_id=%s", existing_order.id, commitment_date, - effective_home_delivery, + is_delivery, consumer_group_id, ) return existing_order @@ -1128,7 +1071,7 @@ class AplicoopWebsiteSale(WebsiteSale): "group_order_id": group_order.id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, - "home_delivery": effective_home_delivery, + "home_delivery": is_delivery, "consumer_group_id": consumer_group_id, } if commitment_date: @@ -1151,25 +1094,23 @@ class AplicoopWebsiteSale(WebsiteSale): sale_order.id, group_order.id, group_order.pickup_day, - effective_home_delivery, + group_order.home_delivery, consumer_group_id, ) return sale_order def _create_draft_sale_order( - self, - group_order, - current_user, - sale_order_lines, - order_id, - pickup_date=None, - is_delivery=False, + self, group_order, current_user, sale_order_lines, order_id, pickup_date=None ): """Create a draft sale.order from prepared lines and propagate group fields. Returns created sale.order record. """ - consumer_group_id = self._validate_user_group_access(group_order, current_user) + # consumer_group_id comes directly from group_order (first/only group) + # No calculations - data propagates directly from the order context + consumer_group_id = ( + group_order.group_ids[0].id if group_order.group_ids else False + ) _logger.info( "[CONSUMER_GROUP DEBUG] _create_draft_sale_order: " @@ -1179,8 +1120,11 @@ class AplicoopWebsiteSale(WebsiteSale): consumer_group_id, ) - effective_home_delivery, commitment_date = self._get_effective_delivery_context( - group_order, is_delivery + # commitment_date comes from group_order (delivery_date or pickup_date) + commitment_date = ( + group_order.delivery_date + if group_order.home_delivery + else group_order.pickup_date ) order_vals = { @@ -1190,7 +1134,7 @@ class AplicoopWebsiteSale(WebsiteSale): "group_order_id": order_id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, - "home_delivery": effective_home_delivery, + "home_delivery": group_order.home_delivery, "consumer_group_id": consumer_group_id, "commitment_date": commitment_date, } @@ -1430,7 +1374,11 @@ class AplicoopWebsiteSale(WebsiteSale): All fields (commitment_date, consumer_group_id, etc.) come from group_order. """ - consumer_group_id = self._validate_user_group_access(group_order, current_user) + # consumer_group_id comes directly from group_order (first/only group) + # No calculations - data propagates directly from the order context + consumer_group_id = ( + group_order.group_ids[0].id if group_order.group_ids else False + ) _logger.info( "[CONSUMER_GROUP DEBUG] _merge_or_replace_draft: " @@ -1440,8 +1388,9 @@ class AplicoopWebsiteSale(WebsiteSale): consumer_group_id, ) - effective_home_delivery, commitment_date = self._get_effective_delivery_context( - group_order, is_delivery + # commitment_date comes directly from selected flow + commitment_date = ( + group_order.delivery_date if is_delivery else group_order.pickup_date ) if existing_drafts: @@ -1457,7 +1406,7 @@ class AplicoopWebsiteSale(WebsiteSale): "group_order_id": order_id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, - "home_delivery": effective_home_delivery, + "home_delivery": is_delivery, "consumer_group_id": consumer_group_id, "commitment_date": commitment_date, } @@ -1471,7 +1420,7 @@ class AplicoopWebsiteSale(WebsiteSale): "group_order_id": order_id, "pickup_day": group_order.pickup_day, "pickup_date": group_order.pickup_date, - "home_delivery": effective_home_delivery, + "home_delivery": is_delivery, "consumer_group_id": consumer_group_id, "commitment_date": commitment_date, } @@ -2291,18 +2240,8 @@ class AplicoopWebsiteSale(WebsiteSale): status=400, ) - try: - self._validate_user_group_access(group_order, current_user) - except ValueError as e: - return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=403, - ) - items = data.get("items", []) pickup_date = data.get("pickup_date") - is_delivery = self._to_bool(data.get("is_delivery", False)) if not items: return request.make_response( json.dumps({"error": "No items in cart"}), @@ -2323,12 +2262,7 @@ class AplicoopWebsiteSale(WebsiteSale): ) sale_order = self._create_draft_sale_order( - group_order, - current_user, - sale_order_lines, - order_id, - pickup_date, - is_delivery=is_delivery, + group_order, current_user, sale_order_lines, order_id, pickup_date ) _logger.info( @@ -2695,15 +2629,6 @@ class AplicoopWebsiteSale(WebsiteSale): status=400, ) - try: - self._validate_user_group_access(group_order, current_user) - except ValueError as e: - return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=403, - ) - # Get cart items items = data.get("items", []) is_delivery = self._to_bool(data.get("is_delivery", False)) @@ -2866,23 +2791,27 @@ class AplicoopWebsiteSale(WebsiteSale): ) existing_order = None - effective_home_delivery, commitment_date = ( - self._get_effective_delivery_context(group_order, is_delivery) - ) + # Get pickup date and delivery info from group order + # If delivery, use delivery_date; otherwise use pickup_date + commitment_date = None + if is_delivery and group_order.delivery_date: + commitment_date = group_order.delivery_date + elif group_order.pickup_date: + commitment_date = group_order.pickup_date # Create or update sale.order using helper sale_order = self._create_or_update_sale_order( group_order, current_user, sale_order_lines, - effective_home_delivery, + is_delivery, commitment_date=commitment_date, existing_order=existing_order, ) # Build confirmation message using helper message_data = self._build_confirmation_message( - sale_order, group_order, effective_home_delivery + sale_order, group_order, is_delivery ) message = message_data["message"] pickup_day_name = message_data["pickup_day"] diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py index e0f4250..20b51bd 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -799,45 +799,9 @@ class GroupOrder(models.Model): self.delivery_date, ) - # Confirm each order in an isolated savepoint so one bad product - # route configuration doesn't block all remaining orders. - confirmed_sale_orders = SaleOrder.browse() - failed_sale_orders = SaleOrder.browse() - - for sale_order in sale_orders: - try: - with self.env.cr.savepoint(): - # Do not block sales confirmation due to procurement - # route issues. This cron confirms business orders first - # and handles stock exceptions operationally. - sale_order.with_context(from_orderpoint=True).action_confirm() - confirmed_sale_orders |= sale_order - except Exception: - failed_sale_orders |= sale_order - _logger.exception( - "Cron: Error confirming sale order %s (%s) for group order %s (%s). " - "Order skipped; remaining orders will continue.", - sale_order.id, - sale_order.name, - self.id, - self.name, - ) - - if confirmed_sale_orders: - # Create picking batches only for confirmed sale orders - self._create_picking_batches_for_sale_orders(confirmed_sale_orders) - self._log_missing_procurement_warnings(confirmed_sale_orders) - - if failed_sale_orders: - _logger.warning( - "Cron: %d/%d sale orders failed during confirmation for group order %s (%s). " - "failed_sale_order_ids=%s", - len(failed_sale_orders), - len(sale_orders), - self.id, - self.name, - failed_sale_orders.ids, - ) + sale_orders.action_confirm() + # Create picking batches after confirmation + self._create_picking_batches_for_sale_orders(sale_orders) except Exception: _logger.exception( "Cron: Error confirming sale orders for group order %s (%s)", @@ -845,42 +809,6 @@ class GroupOrder(models.Model): self.name, ) - def _log_missing_procurement_warnings(self, sale_orders): - """Log warnings for confirmed orders with stockable lines lacking moves. - - This helps operations detect products with missing route/procurement - configuration while still allowing sale order confirmation. - """ - self.ensure_one() - - for sale_order in sale_orders: - problematic_lines = sale_order.order_line.filtered( - lambda line: line.product_id - and getattr(line.product_id, "is_storable", False) - and line.product_uom_qty > 0 - and not line.move_ids - ) - - if not problematic_lines: - continue - - product_labels = [] - for line in problematic_lines: - code = line.product_id.default_code or "NO-CODE" - name = line.product_id.display_name or line.product_id.name - product_labels.append(f"[{code}] {name}") - - _logger.warning( - "Cron: Sale order %s (%s) confirmed but %d stockable lines have no stock moves. " - "Likely missing replenishment routes/rules. group_order=%s (%s), products=%s", - sale_order.id, - sale_order.name, - len(problematic_lines), - self.id, - self.name, - product_labels, - ) - def _create_picking_batches_for_sale_orders(self, sale_orders): """Create stock.picking.batch grouped by consumer_group_id. diff --git a/website_sale_aplicoop/tests/__init__.py b/website_sale_aplicoop/tests/__init__.py index d423f33..2effc65 100644 --- a/website_sale_aplicoop/tests/__init__.py +++ b/website_sale_aplicoop/tests/__init__.py @@ -10,7 +10,6 @@ from . import test_record_rules # noqa: F401 from . import test_multi_company # noqa: F401 from . import test_save_order_endpoints # noqa: F401 from . import test_date_calculations # noqa: F401 -from . import test_phase3_confirm_eskaera # noqa: F401 from . import test_pricing_with_pricelist # noqa: F401 from . import test_portal_sale_order_creation # noqa: F401 from . import test_cron_picking_batch # noqa: F401 diff --git a/website_sale_aplicoop/tests/test_cron_picking_batch.py b/website_sale_aplicoop/tests/test_cron_picking_batch.py index ae04036..d408f8f 100644 --- a/website_sale_aplicoop/tests/test_cron_picking_batch.py +++ b/website_sale_aplicoop/tests/test_cron_picking_batch.py @@ -2,10 +2,8 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) from datetime import timedelta -from unittest.mock import patch from odoo import fields -from odoo.exceptions import UserError from odoo.tests.common import TransactionCase from odoo.tests.common import tagged @@ -404,54 +402,3 @@ class TestCronPickingBatch(TransactionCase): "draft", "Sale order should remain draft - group order is closed", ) - - def test_cron_confirm_ignores_procurement_usererror(self): - """Procurement UserError must not block confirmation in cron flow.""" - group_order = self._create_group_order(cutoff_in_past=True) - - so_ok = self._create_sale_order( - group_order, self.member_1, self.consumer_group_1 - ) - so_fail = self._create_sale_order( - group_order, - self.member_2, - self.consumer_group_2, - ) - - so_ok.write({"name": "SO-OK"}) - so_fail.write({"name": "SO-FAIL"}) - - SaleOrderClass = type(self.env["sale.order"]) - original_action_confirm = SaleOrderClass.action_confirm - - def _patched_action_confirm(recordset): - should_fail = any(so.name == "SO-FAIL" for so in recordset) - if should_fail and not recordset.env.context.get("from_orderpoint"): - raise UserError("Simulated stock route error") - return original_action_confirm(recordset) - - with patch.object(SaleOrderClass, "action_confirm", _patched_action_confirm): - group_order._confirm_linked_sale_orders() - - so_ok.invalidate_recordset() - so_fail.invalidate_recordset() - - self.assertEqual( - so_ok.state, - "sale", - "The valid order must still be confirmed", - ) - self.assertTrue( - so_ok.picking_ids, - "The valid order should have pickings created", - ) - self.assertTrue( - so_ok.picking_ids[0].batch_id, - "Pickings from valid orders should be batched", - ) - - self.assertEqual( - so_fail.state, - "sale", - "The order should be confirmed when cron uses non-blocking procurement context", - ) diff --git a/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py b/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py index f183c0a..3562c90 100644 --- a/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py +++ b/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py @@ -20,108 +20,37 @@ Includes tests for: import json from datetime import date from datetime import timedelta -from types import SimpleNamespace +from unittest.mock import Mock from unittest.mock import patch +from odoo import http from odoo.tests.common import TransactionCase -from odoo.addons.website_sale_aplicoop.controllers.website_sale import ( - AplicoopWebsiteSale, -) - -REQUEST_PATCH_TARGET = ( - "odoo.addons.website_sale_aplicoop.controllers.website_sale.request" -) - - -def _make_json_response(data, headers=None, status=200): - """Build a lightweight HTTP-like response object for controller tests.""" - - raw = data.encode("utf-8") if isinstance(data, str) else data - return SimpleNamespace(data=raw, status=status, headers=headers or []) - - -def _build_request_mock(env, payload=None, website=None): - """Build a request mock compatible with the controller helpers/routes. - - Args: - env: Odoo Environment to use as request.env. - payload: dict that will be JSON-encoded as request.httprequest.data. - website: Optional real website record. When not provided a minimal - SimpleNamespace stub is used (sufficient for tests that do not - call pricing helpers). - """ - if website is None: - website = SimpleNamespace( - _get_current_pricelist=lambda: False, - show_line_subtotals_tax_selection="tax_excluded", - fiscal_position_id=False, - company_id=False, - ) - request_mock = SimpleNamespace( - env=env, - make_response=_make_json_response, - website=website, - ) - if payload is not None: - request_mock.httprequest = SimpleNamespace( - data=json.dumps(payload).encode("utf-8") - ) - return request_mock - class TestValidateConfirmJson(TransactionCase): """Test _validate_confirm_json() helper method.""" def setUp(self): super().setUp() - self.controller = AplicoopWebsiteSale() - self.group_1 = self.env["res.partner"].create( - { - "name": "Consumer Group 1", - "is_company": True, - "is_group": True, - } - ) - self.group_2 = self.env["res.partner"].create( - { - "name": "Consumer Group 2", - "is_company": True, - "is_group": True, - } - ) - self.partner = self.env["res.partner"].create( - { - "name": "Phase 3 Member", - "email": "phase3-member@test.com", - } - ) - self.group_2.member_ids = [(4, self.partner.id)] - self.user = self.env["res.users"].create( - { - "name": "Phase 3 User", - "login": "phase3-user@test.com", - "email": "phase3-user@test.com", - "partner_id": self.partner.id, - } - ) + self.controller = http.request.env["website.sale"].browse([]) + self.user = self.env.ref("base.user_admin") + self.partner = self.env.ref("base.partner_admin") # Create test group order self.group_order = self.env["group.order"].create( { "name": "Test Order Phase 3", "state": "open", - "group_ids": [(6, 0, [self.group_1.id, self.group_2.id])], - "start_date": date.today(), - "end_date": date.today() + timedelta(days=7), + "collection_date": date.today() + timedelta(days=3), "cutoff_day": "3", # Thursday "pickup_day": "5", # Saturday } ) - def test_validate_confirm_json_success(self): + @patch("odoo.http.request") + def test_validate_confirm_json_success(self, mock_request): """Test successful validation of confirm JSON data.""" - request_mock = _build_request_mock(self.env(user=self.user)) + mock_request.env = self.env.with_user(self.user) data = { "order_id": self.group_order.id, @@ -129,10 +58,9 @@ class TestValidateConfirmJson(TransactionCase): "is_delivery": False, } - with patch(REQUEST_PATCH_TARGET, request_mock): - order_id, group_order, current_user, items, is_delivery = ( - self.controller._validate_confirm_json(data) - ) + order_id, group_order, current_user, items, is_delivery = ( + self.controller._validate_confirm_json(data) + ) self.assertEqual(order_id, self.group_order.id) self.assertEqual(group_order.id, self.group_order.id) @@ -140,51 +68,52 @@ class TestValidateConfirmJson(TransactionCase): self.assertEqual(len(items), 1) self.assertFalse(is_delivery) - def test_validate_confirm_json_missing_order_id(self): + @patch("odoo.http.request") + def test_validate_confirm_json_missing_order_id(self, mock_request): """Test validation fails without order_id.""" - request_mock = _build_request_mock(self.env(user=self.user)) + mock_request.env = self.env.with_user(self.user) data = {"items": [{"product_id": 1, "quantity": 2}]} - with patch(REQUEST_PATCH_TARGET, request_mock): - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) + with self.assertRaises(ValueError) as context: + self.controller._validate_confirm_json(data) - self.assertIn("order_id is required", str(context.exception)) + self.assertIn("Missing order_id", str(context.exception)) - def test_validate_confirm_json_order_not_exists(self): + @patch("odoo.http.request") + def test_validate_confirm_json_order_not_exists(self, mock_request): """Test validation fails with non-existent order.""" - request_mock = _build_request_mock(self.env(user=self.user)) + mock_request.env = self.env.with_user(self.user) data = { "order_id": 99999, # Non-existent ID "items": [{"product_id": 1, "quantity": 2}], } - with patch(REQUEST_PATCH_TARGET, request_mock): - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) + with self.assertRaises(ValueError) as context: + self.controller._validate_confirm_json(data) self.assertIn("Order", str(context.exception)) - def test_validate_confirm_json_no_items(self): + @patch("odoo.http.request") + def test_validate_confirm_json_no_items(self, mock_request): """Test validation fails without items in cart.""" - request_mock = _build_request_mock(self.env(user=self.user)) + mock_request.env = self.env.with_user(self.user) data = { "order_id": self.group_order.id, "items": [], } - with patch(REQUEST_PATCH_TARGET, request_mock): - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) + with self.assertRaises(ValueError) as context: + self.controller._validate_confirm_json(data) self.assertIn("No items in cart", str(context.exception)) - def test_validate_confirm_json_with_delivery_flag(self): + @patch("odoo.http.request") + def test_validate_confirm_json_with_delivery_flag(self, mock_request): """Test validation correctly handles is_delivery flag.""" - request_mock = _build_request_mock(self.env(user=self.user)) + mock_request.env = self.env.with_user(self.user) data = { "order_id": self.group_order.id, @@ -192,165 +121,17 @@ class TestValidateConfirmJson(TransactionCase): "is_delivery": True, } - with patch(REQUEST_PATCH_TARGET, request_mock): - _, _, _, _, is_delivery = self.controller._validate_confirm_json(data) + _, _, _, _, is_delivery = self.controller._validate_confirm_json(data) self.assertTrue(is_delivery) - def test_validate_confirm_json_user_without_matching_group(self): - """Validation must fail if user is not in any allowed consumer group.""" - other_partner = self.env["res.partner"].create( - { - "name": "Partner without matching group", - "email": "nogroup@test.com", - } - ) - other_user = self.env["res.users"].create( - { - "name": "User without matching group", - "login": "nogroup-user@test.com", - "email": "nogroup-user@test.com", - "partner_id": other_partner.id, - } - ) - request_mock = _build_request_mock(self.env(user=other_user)) - - data = { - "order_id": self.group_order.id, - "items": [{"product_id": 1, "quantity": 1}], - } - - with patch(REQUEST_PATCH_TARGET, request_mock): - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) - - self.assertIn( - "User is not a member of any consumer group in this order", - str(context.exception), - ) - - -class TestConsumerGroupResolution(TransactionCase): - """Tests for consumer group selection in multi-group orders.""" - - def setUp(self): - super().setUp() - self.controller = AplicoopWebsiteSale() - self.group_1 = self.env["res.partner"].create( - { - "name": "Group A", - "is_company": True, - "is_group": True, - } - ) - self.group_2 = self.env["res.partner"].create( - { - "name": "Group B", - "is_company": True, - "is_group": True, - } - ) - self.group_3 = self.env["res.partner"].create( - { - "name": "Group C", - "is_company": True, - "is_group": True, - } - ) - self.partner = self.env["res.partner"].create( - { - "name": "Member B", - "email": "memberb@test.com", - } - ) - self.group_2.member_ids = [(4, self.partner.id)] - self.user = self.env["res.users"].create( - { - "name": "Member B User", - "login": "memberb-user@test.com", - "email": "memberb-user@test.com", - "partner_id": self.partner.id, - } - ) - self.product = self.env["product.product"].create( - { - "name": "Group Resolution Product", - "type": "consu", - "list_price": 10.0, - } - ) - self.group_order = self.env["group.order"].create( - { - "name": "Multi-group order", - "state": "open", - "group_ids": [(6, 0, [self.group_1.id, self.group_2.id])], - "start_date": date.today(), - "end_date": date.today() + timedelta(days=7), - "cutoff_day": "1", - "pickup_day": "2", - } - ) - self.sale_order_lines = [ - ( - 0, - 0, - { - "product_id": self.product.id, - "name": "Test line", - "product_uom_qty": 1, - "price_unit": 10.0, - }, - ) - ] - - def test_get_consumer_group_for_user_returns_matching_group(self): - """Should resolve the user's own consumer group, not the first order group.""" - consumer_group_id = self.controller._get_consumer_group_for_user( - self.group_order, self.user - ) - - self.assertEqual(consumer_group_id, self.group_2.id) - - def test_get_consumer_group_for_user_uses_first_matching_order_group(self): - """If user belongs to multiple valid groups, use the first in group_order.""" - self.group_1.member_ids = [(4, self.partner.id)] - group_order = self.env["group.order"].create( - { - "name": "Multi-match order", - "state": "open", - "group_ids": [ - (6, 0, [self.group_1.id, self.group_2.id, self.group_3.id]) - ], - } - ) - - consumer_group_id = self.controller._get_consumer_group_for_user( - group_order, self.user - ) - - self.assertEqual(consumer_group_id, self.group_1.id) - - def test_create_or_update_sale_order_assigns_user_consumer_group(self): - """Created sale.order must use the consumer group that matches the user.""" - request_mock = _build_request_mock(self.env(user=self.user)) - - with patch(REQUEST_PATCH_TARGET, request_mock): - sale_order = self.controller._create_or_update_sale_order( - self.group_order, - self.user, - self.sale_order_lines, - is_delivery=False, - ) - - self.assertEqual(sale_order.consumer_group_id.id, self.group_2.id) - class TestProcessCartItems(TransactionCase): """Test _process_cart_items() helper method.""" def setUp(self): super().setUp() - self.controller = AplicoopWebsiteSale() + self.controller = http.request.env["website.sale"].browse([]) # Create test products self.product1 = self.env["product.product"].create( @@ -367,28 +148,20 @@ class TestProcessCartItems(TransactionCase): "type": "consu", } ) - self.group = self.env["res.partner"].create( - { - "name": "Cart Test Group", - "is_company": True, - "is_group": True, - } - ) # Create test group order self.group_order = self.env["group.order"].create( { "name": "Test Order for Cart", "state": "open", - "group_ids": [(6, 0, [self.group.id])], - "start_date": date.today(), - "end_date": date.today() + timedelta(days=7), } ) - def test_process_cart_items_success(self): + @patch("odoo.http.request") + def test_process_cart_items_success(self, mock_request): """Test successful cart item processing.""" - request_mock = _build_request_mock(self.env.with_context(lang="es_ES")) + mock_request.env = self.env + mock_request.env.lang = "es_ES" items = [ { @@ -403,8 +176,7 @@ class TestProcessCartItems(TransactionCase): }, ] - with patch(REQUEST_PATCH_TARGET, request_mock): - result = self.controller._process_cart_items(items, self.group_order) + result = self.controller._process_cart_items(items, self.group_order) self.assertEqual(len(result), 2) self.assertEqual(result[0][0], 0) # Command (0, 0, vals) @@ -413,9 +185,11 @@ class TestProcessCartItems(TransactionCase): self.assertEqual(result[0][2]["product_uom_qty"], 2) self.assertEqual(result[0][2]["price_unit"], 15.0) - def test_process_cart_items_uses_list_price_fallback(self): + @patch("odoo.http.request") + def test_process_cart_items_uses_list_price_fallback(self, mock_request): """Test cart processing uses list_price when product_price is 0.""" - request_mock = _build_request_mock(self.env.with_context(lang="es_ES")) + mock_request.env = self.env + mock_request.env.lang = "es_ES" items = [ { @@ -425,16 +199,17 @@ class TestProcessCartItems(TransactionCase): } ] - with patch(REQUEST_PATCH_TARGET, request_mock): - result = self.controller._process_cart_items(items, self.group_order) + result = self.controller._process_cart_items(items, self.group_order) self.assertEqual(len(result), 1) # Should use product.list_price as fallback self.assertEqual(result[0][2]["price_unit"], self.product1.list_price) - def test_process_cart_items_skips_invalid_product(self): + @patch("odoo.http.request") + def test_process_cart_items_skips_invalid_product(self, mock_request): """Test cart processing skips non-existent products.""" - request_mock = _build_request_mock(self.env.with_context(lang="es_ES")) + mock_request.env = self.env + mock_request.env.lang = "es_ES" items = [ { @@ -449,28 +224,30 @@ class TestProcessCartItems(TransactionCase): }, ] - with patch(REQUEST_PATCH_TARGET, request_mock): - result = self.controller._process_cart_items(items, self.group_order) + result = self.controller._process_cart_items(items, self.group_order) # Should only process the valid product self.assertEqual(len(result), 1) self.assertEqual(result[0][2]["product_id"], self.product1.id) - def test_process_cart_items_empty_after_filtering(self): + @patch("odoo.http.request") + def test_process_cart_items_empty_after_filtering(self, mock_request): """Test cart processing raises error when no valid items remain.""" - request_mock = _build_request_mock(self.env.with_context(lang="es_ES")) + mock_request.env = self.env + mock_request.env.lang = "es_ES" items = [{"product_id": 99999, "quantity": 1, "product_price": 10.0}] - with patch(REQUEST_PATCH_TARGET, request_mock): - with self.assertRaises(ValueError) as context: - self.controller._process_cart_items(items, self.group_order) + with self.assertRaises(ValueError) as context: + self.controller._process_cart_items(items, self.group_order) self.assertIn("No valid items", str(context.exception)) - def test_process_cart_items_translates_product_name(self): + @patch("odoo.http.request") + def test_process_cart_items_translates_product_name(self, mock_request): """Test cart processing uses translated product names.""" - request_mock = _build_request_mock(self.env.with_context(lang="eu_ES")) + mock_request.env = self.env + mock_request.env.lang = "eu_ES" # Basque # Add translation for product name self.env["ir.translation"].create( @@ -494,8 +271,7 @@ class TestProcessCartItems(TransactionCase): } ] - with patch(REQUEST_PATCH_TARGET, request_mock): - result = self.controller._process_cart_items(items, self.group_order) + result = self.controller._process_cart_items(items, self.group_order) # Product name should be in Basque context product_name = result[0][2]["name"] @@ -508,27 +284,21 @@ class TestBuildConfirmationMessage(TransactionCase): def setUp(self): super().setUp() - self.controller = AplicoopWebsiteSale() + self.controller = http.request.env["website.sale"].browse([]) self.user = self.env.ref("base.user_admin") self.partner = self.env.ref("base.partner_admin") - self.group = self.env["res.partner"].create( - { - "name": "Message Test Group", - "is_company": True, - "is_group": True, - } - ) # Create test group order with dates + pickup_date = date.today() + timedelta(days=5) + delivery_date = pickup_date + timedelta(days=1) + self.group_order = self.env["group.order"].create( { "name": "Test Order Messages", "state": "open", - "group_ids": [(6, 0, [self.group.id])], - "start_date": date.today(), - "end_date": date.today() + timedelta(days=7), - "cutoff_day": "3", "pickup_day": "5", # Saturday (0=Monday) + "pickup_date": pickup_date, + "delivery_date": delivery_date, } ) @@ -540,7 +310,7 @@ class TestBuildConfirmationMessage(TransactionCase): } ) - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_pickup(self, mock_request): """Test confirmation message for pickup (not delivery).""" mock_request.env = self.env.with_context(lang="es_ES") @@ -563,7 +333,7 @@ class TestBuildConfirmationMessage(TransactionCase): # Should have pickup day index self.assertEqual(result["pickup_day_index"], 5) - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_delivery(self, mock_request): """Test confirmation message for home delivery.""" mock_request.env = self.env.with_context(lang="es_ES") @@ -583,7 +353,7 @@ class TestBuildConfirmationMessage(TransactionCase): # pickup_day_index=5 (Saturday), delivery should be 6 (Sunday) # Note: _get_day_names would need to be mocked for exact day name - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_no_dates(self, mock_request): """Test confirmation message when no pickup date is set.""" mock_request.env = self.env.with_context(lang="es_ES") @@ -593,7 +363,6 @@ class TestBuildConfirmationMessage(TransactionCase): { "name": "Order No Dates", "state": "open", - "group_ids": [(6, 0, [self.group.id])], } ) @@ -615,7 +384,7 @@ class TestBuildConfirmationMessage(TransactionCase): # Date fields should be empty self.assertEqual(result["pickup_date"], "") - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_formats_date(self, mock_request): """Test confirmation message formats dates correctly (DD/MM/YYYY).""" mock_request.env = self.env.with_context(lang="es_ES") @@ -633,7 +402,7 @@ class TestBuildConfirmationMessage(TransactionCase): date_pattern = r"\d{2}/\d{2}/\d{4}" self.assertRegex(pickup_date_str, date_pattern) - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_es(self, mock_request): """Test confirmation message in Spanish (es_ES).""" mock_request.env = self.env.with_context(lang="es_ES") @@ -647,7 +416,7 @@ class TestBuildConfirmationMessage(TransactionCase): self.assertIsNotNone(message) # In real scenario, would check for "¡Gracias!" or similar - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_eu(self, mock_request): """Test confirmation message in Basque (eu_ES).""" mock_request.env = self.env.with_context(lang="eu_ES") @@ -660,7 +429,7 @@ class TestBuildConfirmationMessage(TransactionCase): self.assertIsNotNone(message) # In real scenario, would check for "Eskerrik asko!" or similar - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_ca(self, mock_request): """Test confirmation message in Catalan (ca_ES).""" mock_request.env = self.env.with_context(lang="ca_ES") @@ -673,7 +442,7 @@ class TestBuildConfirmationMessage(TransactionCase): self.assertIsNotNone(message) # In real scenario, would check for "Gràcies!" or similar - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_gl(self, mock_request): """Test confirmation message in Galician (gl_ES).""" mock_request.env = self.env.with_context(lang="gl_ES") @@ -686,7 +455,7 @@ class TestBuildConfirmationMessage(TransactionCase): self.assertIsNotNone(message) # In real scenario, would check for "Grazas!" or similar - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_pt(self, mock_request): """Test confirmation message in Portuguese (pt_PT).""" mock_request.env = self.env.with_context(lang="pt_PT") @@ -699,7 +468,7 @@ class TestBuildConfirmationMessage(TransactionCase): self.assertIsNotNone(message) # In real scenario, would check for "Obrigado!" or similar - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_fr(self, mock_request): """Test confirmation message in French (fr_FR).""" mock_request.env = self.env.with_context(lang="fr_FR") @@ -712,7 +481,7 @@ class TestBuildConfirmationMessage(TransactionCase): self.assertIsNotNone(message) # In real scenario, would check for "Merci!" or similar - @patch(REQUEST_PATCH_TARGET) + @patch("odoo.http.request") def test_build_confirmation_message_multilang_it(self, mock_request): """Test confirmation message in Italian (it_IT).""" mock_request.env = self.env.with_context(lang="it_IT") @@ -731,29 +500,9 @@ class TestConfirmEskaera_Integration(TransactionCase): def setUp(self): super().setUp() - self.controller = AplicoopWebsiteSale() - self.consumer_group = self.env["res.partner"].create( - { - "name": "Integration Consumer Group", - "is_company": True, - "is_group": True, - } - ) - self.partner = self.env["res.partner"].create( - { - "name": "Integration Member", - "email": "integration-member@test.com", - } - ) - self.consumer_group.member_ids = [(4, self.partner.id)] - self.user = self.env["res.users"].create( - { - "name": "Integration User", - "login": "integration-user@test.com", - "email": "integration-user@test.com", - "partner_id": self.partner.id, - } - ) + self.controller = http.request.env["website.sale"].browse([]) + self.user = self.env.ref("base.user_admin") + self.partner = self.env.ref("base.partner_admin") # Create test product self.product = self.env["product.product"].create( @@ -769,20 +518,19 @@ class TestConfirmEskaera_Integration(TransactionCase): { "name": "Integration Test Order", "state": "open", - "group_ids": [(6, 0, [self.consumer_group.id])], - "start_date": date.today(), - "end_date": date.today() + timedelta(days=7), - "cutoff_day": "3", "pickup_day": "5", - "home_delivery": True, + "pickup_date": date.today() + timedelta(days=5), } ) - # Use the real website so pricing helpers can resolve company/fiscal pos - self.website = self.env["website"].search([], limit=1) - - def test_confirm_eskaera_full_flow_pickup(self): + @patch("odoo.http.request") + def test_confirm_eskaera_full_flow_pickup(self, mock_request): """Test full confirm_eskaera flow for pickup order.""" + mock_request.env = self.env.with_user(self.user) + mock_request.env.lang = "es_ES" + mock_request.httprequest = Mock() + + # Prepare request data data = { "order_id": self.group_order.id, "items": [ @@ -794,14 +542,11 @@ class TestConfirmEskaera_Integration(TransactionCase): ], "is_delivery": False, } - mock_request = _build_request_mock( - self.env(user=self.user, context=dict(self.env.context, lang="es_ES")), - data, - website=self.website, - ) - with patch(REQUEST_PATCH_TARGET, mock_request): - response = self.controller.confirm_eskaera.__wrapped__(self.controller) + mock_request.httprequest.data = json.dumps(data).encode("utf-8") + + # Call confirm_eskaera + response = self.controller.confirm_eskaera() # Verify response self.assertIsNotNone(response) @@ -820,19 +565,20 @@ class TestConfirmEskaera_Integration(TransactionCase): self.assertEqual(sale_order.group_order_id.id, self.group_order.id) self.assertEqual(len(sale_order.order_line), 1) self.assertEqual(sale_order.order_line[0].product_uom_qty, 3) - self.assertFalse(sale_order.home_delivery) - self.assertEqual( - sale_order.commitment_date.date(), - self.group_order.pickup_date, - ) - def test_confirm_eskaera_full_flow_delivery(self): + @patch("odoo.http.request") + def test_confirm_eskaera_full_flow_delivery(self, mock_request): """Test full confirm_eskaera flow for delivery order.""" + mock_request.env = self.env.with_user(self.user) + mock_request.env.lang = "es_ES" + mock_request.httprequest = Mock() + # Add delivery_date to group order self.group_order.delivery_date = self.group_order.pickup_date + timedelta( days=1 ) + # Prepare request data data = { "order_id": self.group_order.id, "items": [ @@ -844,14 +590,11 @@ class TestConfirmEskaera_Integration(TransactionCase): ], "is_delivery": True, } - mock_request = _build_request_mock( - self.env(user=self.user, context=dict(self.env.context, lang="es_ES")), - data, - website=self.website, - ) - with patch(REQUEST_PATCH_TARGET, mock_request): - response = self.controller.confirm_eskaera.__wrapped__(self.controller) + mock_request.httprequest.data = json.dumps(data).encode("utf-8") + + # Call confirm_eskaera + response = self.controller.confirm_eskaera() # Verify response response_data = json.loads(response.data.decode("utf-8")) @@ -868,17 +611,19 @@ class TestConfirmEskaera_Integration(TransactionCase): sale_order.commitment_date.date(), self.group_order.delivery_date ) - def test_confirm_eskaera_updates_existing_draft(self): + @patch("odoo.http.request") + def test_confirm_eskaera_updates_existing_draft(self, mock_request): """Test confirm_eskaera updates existing draft order instead of creating new.""" + mock_request.env = self.env.with_user(self.user) + mock_request.env.lang = "es_ES" + mock_request.httprequest = Mock() + # Create existing draft order existing_order = self.env["sale.order"].create( { "partner_id": self.partner.id, "group_order_id": self.group_order.id, "state": "draft", - # pickup_date must match group_order.pickup_date so - # _find_recent_draft_order can locate this draft - "pickup_date": self.group_order.pickup_date, "order_line": [ ( 0, @@ -895,6 +640,7 @@ class TestConfirmEskaera_Integration(TransactionCase): existing_order_id = existing_order.id + # Prepare new request data data = { "order_id": self.group_order.id, "items": [ @@ -906,14 +652,11 @@ class TestConfirmEskaera_Integration(TransactionCase): ], "is_delivery": False, } - mock_request = _build_request_mock( - self.env(user=self.user, context=dict(self.env.context, lang="es_ES")), - data, - website=self.website, - ) - with patch(REQUEST_PATCH_TARGET, mock_request): - response = self.controller.confirm_eskaera.__wrapped__(self.controller) + mock_request.httprequest.data = json.dumps(data).encode("utf-8") + + # Call confirm_eskaera + response = self.controller.confirm_eskaera() response_data = json.loads(response.data.decode("utf-8")) @@ -925,8 +668,13 @@ class TestConfirmEskaera_Integration(TransactionCase): self.assertEqual(len(existing_order.order_line), 1) self.assertEqual(existing_order.order_line[0].product_uom_qty, 5) - def test_confirm_eskaera_ignores_old_period_draft(self): + @patch("odoo.http.request") + def test_confirm_eskaera_ignores_old_period_draft(self, mock_request): """Old draft from previous pickup_date must not be reused.""" + mock_request.env = self.env.with_user(self.user) + mock_request.env.lang = "es_ES" + mock_request.httprequest = Mock() + old_draft = self.env["sale.order"].create( { "partner_id": self.partner.id, @@ -958,15 +706,10 @@ class TestConfirmEskaera_Integration(TransactionCase): ], "is_delivery": False, } - mock_request = _build_request_mock( - self.env(user=self.user, context=dict(self.env.context, lang="es_ES")), - data, - website=self.website, - ) - with patch(REQUEST_PATCH_TARGET, mock_request): - response = self.controller.confirm_eskaera.__wrapped__(self.controller) + mock_request.httprequest.data = json.dumps(data).encode("utf-8") + response = self.controller.confirm_eskaera() response_data = json.loads(response.data.decode("utf-8")) self.assertTrue(response_data.get("success"))