diff --git a/models/stock_location.py b/models/stock_location.py index 97b1750..70c62a0 100644 --- a/models/stock_location.py +++ b/models/stock_location.py @@ -19,25 +19,33 @@ class StockLocation(models.Model): """ ctx = self.env.context - # 1. PRIORITY: Check the context for allowed locations (Passed by views) - # This is the most reliable source in the MO "Add line" catalog/details view. - # It handles both 'allowed_source_location_ids' and 'default_allowed_source_location_ids' - context_ids = (ctx.get('allowed_source_location_ids') or - ctx.get('default_allowed_source_location_ids')) - - if context_ids: - # Handle list of IDs, or Command tuples (e.g. [Command.set([ID1, ID2])]) - if isinstance(context_ids, list): - # Simple list of IDs - if all(isinstance(x, int) for x in context_ids): - return context_ids - # Command format: [(6, 0, [ID1, ID2])] - for cmd in context_ids: - if isinstance(cmd, (list, tuple)) and cmd[0] == 6: - return cmd[2] - # Single ID - if isinstance(context_ids, int): - return [context_ids] + # 1. PRIORITY: Check the context for allowed locations (Passed by views/JS) + target_keys = ['allowed_source_location_ids', 'default_allowed_source_location_ids'] + for key in target_keys: + val = ctx.get(key) + if not val: + continue + + # Case: List of IDs or Commands + if isinstance(val, list): + # Simple list of integers + if val and all(isinstance(x, int) for x in val): + return val + + # Command format: [(6, 0, [IDs]), (4, ID, 0), ...] + col_ids = [] + for entry in val: + if isinstance(entry, (list, tuple)): + if entry[0] == 6: # SET command + return entry[2] + if entry[0] == 4: # LINK command + col_ids.append(entry[1]) + if col_ids: + return col_ids + + # Case: Single ID + if isinstance(val, int): + return [val] # 2. FALLBACK: Identify the Picking Type (Operation Type) picking_type_id = (ctx.get('default_picking_type_id') or diff --git a/static/src/js/stock_move_line_x2_many_field_patch.js b/static/src/js/stock_move_line_x2_many_field_patch.js index cb6860b..cf13fb3 100644 --- a/static/src/js/stock_move_line_x2_many_field_patch.js +++ b/static/src/js/stock_move_line_x2_many_field_patch.js @@ -1,43 +1,71 @@ /** @odoo-module **/ +import { _t } from "@web/core/l10n/translation"; +import { Domain } from "@web/core/domain"; import { patch } from "@web/core/utils/patch"; import { SMLX2ManyField } from "@stock/fields/stock_move_line_x2_many_field"; patch(SMLX2ManyField.prototype, { async onAdd({ context, editable } = {}) { - // Inject active_mo_id into context - if (this.props.context.active_mo_id) { - context = { - ...context, - active_mo_id: this.props.context.active_mo_id, - }; - } else if (this.props.record.model.config.resModel === "stock.move" && this.props.record.data.raw_material_production_id) { - // Fallback: try to get MO ID from the move record if available (though raw_material_production_id might be a relation) - // The most reliable way is if it was passed in props.context which we did in XML - if (this.props.record.data.raw_material_production_id[0]) { - context = { - ...context, - active_mo_id: this.props.record.data.raw_material_production_id[0], - }; + // Only apply our custom logic when Odoo shows the Quant selection modal (catalog) + if (!this.props.record.data.show_quant) { + return super.onAdd({ context, editable }); + } + + // 1. Sync Dirty Data (Internal Odoo Logic) + await this.updateDirtyQuantsData(); + + // 2. Prepare Context (Bridge to Python) + context = { + ...context, + single_product: true, + list_view_ref: "stock.view_stock_quant_tree_simple", + // Pass identification keys to our Python _get_allowed_locations() + active_mo_id: context?.active_mo_id || this.props.context.active_mo_id || this.props.context.default_production_id, + default_picking_type_id: context?.default_picking_type_id || this.props.context.default_picking_type_id, + default_allowed_source_location_ids: context?.default_allowed_source_location_ids || this.props.context.default_allowed_source_location_ids, + }; + + const productName = this.props.record.data.product_id.display_name; + const title = _t("Add line: %s", productName); + + // 3. Construct Domain (The Fix) + // Odoo natively uses ODOO_DEFAULT_LOCATION_ID which is just ONE location. + // We override this to use ALL ALLOWED locations if they exist. + const allowedLocationIds = context.default_allowed_source_location_ids; + const targetLocation = allowedLocationIds || this.props.context.default_location_id; + + let domain = [ + ["product_id", "=", this.props.record.data.product_id.id], + ["location_id", "child_of", targetLocation], + ["quantity", ">", 0.0], + ]; + + if (this.quantListViewShowOnHandOnly) { + domain.push(["on_hand", "=", true]); + } + + // 4. Filter out fully used quants (Internal Odoo Logic) + if (this.dirtyQuantsData.size) { + const notFullyUsed = []; + const fullyUsed = []; + for (const [quantId, quantData] of this.dirtyQuantsData.entries()) { + if (quantData.available_quantity > 0) { + notFullyUsed.push(quantId); + } else { + fullyUsed.push(quantId); + } + } + if (fullyUsed.length) { + // Combine domains using Odoo's Domain utility + domain = Domain.and([domain, [["id", "not in", fullyUsed]]]).toList(); + } + if (notFullyUsed.length) { + domain = Domain.or([domain, [["id", "in", notFullyUsed]]]).toList(); } } - // Also inject picking_type_id if available in props.context - if (this.props.context.default_picking_type_id) { - context = { - ...context, - default_picking_type_id: this.props.context.default_picking_type_id, - }; - } - - // Final Sync: ensure allowed locations from the parent view are passed to the modal - if (this.props.context.default_allowed_source_location_ids) { - context = { - ...context, - default_allowed_source_location_ids: this.props.context.default_allowed_source_location_ids, - }; - } - - return super.onAdd({ context, editable }); + // 5. Open the selection modal with our custom domain + return this.selectCreate({ domain, context, title }); } });