From d45f6ab2267f4cf3c2419e7274b2c550b00a796d Mon Sep 17 00:00:00 2001 From: William Henrotin Date: Wed, 5 Apr 2023 14:36:08 +0000 Subject: [PATCH] [IMP] stock: create stock move lines from quants In a flow where the reservation is used (internal transfers, deliveries, ...) adding a new stock move line is now made from the quantities available in stock. The 'add a line' button in the show detail wizard trigger the quant list view to directly pick the wanted lot or location where the stock is available. Only the quantity done is needed to be updated before the validation. This commit remove the 'quant reserve wizard' as the behaviour is an extension of it. Task: 3256447 Part-of: odoo/odoo#117513 --- .../wizard/stock_quant_reserve.py | 13 ---- .../wizard/stock_quant_reserve.xml | 15 ---- addons/stock/models/stock_move.py | 14 ++-- addons/stock/models/stock_move_line.py | 44 ++++++++---- addons/stock/models/stock_quant.py | 14 ++++ .../fields/stock_move_line_x2_many_field.js | 47 +++++++++++++ .../static/src/widgets/stock_pick_from.js | 53 +++++++++++++++ addons/stock/views/stock_move_views.xml | 24 ++++--- addons/stock/views/stock_quant_views.xml | 30 ++++++++ addons/stock/wizard/stock_quant_reserve.py | 68 ------------------- addons/stock/wizard/stock_quant_reserve.xml | 43 ------------ 11 files changed, 199 insertions(+), 166 deletions(-) delete mode 100644 addons/product_expiry/wizard/stock_quant_reserve.py delete mode 100644 addons/product_expiry/wizard/stock_quant_reserve.xml create mode 100644 addons/stock/static/src/fields/stock_move_line_x2_many_field.js create mode 100644 addons/stock/static/src/widgets/stock_pick_from.js delete mode 100644 addons/stock/wizard/stock_quant_reserve.py delete mode 100644 addons/stock/wizard/stock_quant_reserve.xml diff --git a/addons/product_expiry/wizard/stock_quant_reserve.py b/addons/product_expiry/wizard/stock_quant_reserve.py deleted file mode 100644 index 74657173f64..00000000000 --- a/addons/product_expiry/wizard/stock_quant_reserve.py +++ /dev/null @@ -1,13 +0,0 @@ -from odoo import fields, models - - -class ReserveStockQuantLine(models.TransientModel): - _inherit = 'stock.quant.reserve.line' - - expiration_date = fields.Datetime(related='lot_id.expiration_date') - - -class ReserveStockQuant(models.TransientModel): - _inherit = 'stock.quant.reserve' - - show_expiry = fields.Boolean(related='move_id.product_id.use_expiration_date') diff --git a/addons/product_expiry/wizard/stock_quant_reserve.xml b/addons/product_expiry/wizard/stock_quant_reserve.xml deleted file mode 100644 index 994a86a0329..00000000000 --- a/addons/product_expiry/wizard/stock_quant_reserve.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - stock.quant.reserve.inherit - stock.quant.reserve - - - - - - - - - - - diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index cc08ac431d9..c8541bf02a9 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -768,7 +768,7 @@ Please change the quantity done or the rounding precision of your unit of measur if self.product_id.tracking == "serial" and self.state == "assigned": self.next_serial = self.env['stock.lot']._get_next_serial(self.company_id, self.product_id) - + quant_mode = self.picking_type_id.code != 'incoming' return { 'name': _('Detailed Operations'), 'type': 'ir.actions.act_window', @@ -780,13 +780,13 @@ Please change the quantity done or the rounding precision of your unit of measur 'res_id': self.id, 'context': dict( self.env.context, - show_owner=self.picking_type_id.code != 'incoming', - show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`. + show_owner=not quant_mode, + show_quant=quant_mode, + show_lots_m2o=not quant_mode and self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`. show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id, - show_source_location=self.picking_type_id.code != 'incoming', - show_destination_location=self.picking_type_id.code != 'outgoing', - show_package=not self.location_id.usage == 'supplier', - show_reserved_quantity=self.state != 'done' and self.picking_type_id.code != 'incoming' + show_destination_location=not quant_mode, + show_package=not quant_mode, + show_reserved_quantity=self.state != 'done' and quant_mode ), } diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 9287a3667bb..f3f4b943b88 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -37,7 +37,8 @@ class StockMoveLine(models.Model): compute='_compute_reserved_qty', inverse='_set_reserved_qty', store=True) reserved_uom_qty = fields.Float( 'Reserved', default=0.0, digits='Product Unit of Measure', required=True, copy=False) - qty_done = fields.Float('Done', default=0.0, digits='Product Unit of Measure', copy=False) + qty_done = fields.Float('Done', default=0.0, digits='Product Unit of Measure', copy=False, + compute='_compute_qty_done', store=True, readonly=False) package_id = fields.Many2one( 'stock.quant.package', 'Source Package', ondelete='restrict', check_company=True, @@ -82,6 +83,7 @@ class StockMoveLine(models.Model): tracking = fields.Selection(related='product_id.tracking', readonly=True) origin = fields.Char(related='move_id.origin', string='Source') description_picking = fields.Text(string="Description picking") + quant_id = fields.Many2one('stock.quant', "Pick From", store=False) # Dummy field for the detailed operation view @api.depends('product_uom_id.category_id', 'product_id.uom_id.category_id', 'move_id.product_uom', 'product_id.uom_id') def _compute_product_uom_id(self): @@ -131,6 +133,12 @@ class StockMoveLine(models.Model): detect errors. """ raise UserError(_('The requested operation cannot be processed because of a programming error setting the `reserved_qty` field instead of the `reserved_uom_qty`.')) + @api.depends('quant_id') + def _compute_qty_done(self): + for record in self: + if not record.qty_done: + record.qty_done = min(record.quant_id.quantity, record.move_id.product_qty) + @api.constrains('lot_id', 'product_id') def _check_lot_product(self): for line in self: @@ -288,6 +296,8 @@ class StockMoveLine(models.Model): vals['company_id'] = self.env['stock.move'].browse(vals['move_id']).company_id.id elif vals.get('picking_id'): vals['company_id'] = self.env['stock.picking'].browse(vals['picking_id']).company_id.id + if vals.get('quant_id'): + vals.update(self._copy_quant_info(vals)) mls = super().create(vals_list) @@ -379,6 +389,8 @@ class StockMoveLine(models.Model): ('owner_id', 'res.partner'), ('product_uom_id', 'uom.uom') ] + if vals.get('quant_id'): + vals.update(self._copy_quant_info(vals)) updates = {} for key, model in triggers: if key in vals: @@ -872,6 +884,25 @@ class StockMoveLine(models.Model): 'partner_id': self.picking_id.partner_id.id, } + def _copy_quant_info(self, vals): + quant = self.env['stock.quant'].browse(vals.get('quant_id', 0)) + if self: + uom = self.product_uom_id + elif vals.get('product_uom_id'): + uom = self.env['uom.uom'].browse(vals.get('product_uom_id')) + else: + uom = quant.product_uom_id + line_data = { + 'product_id': quant.product_id.id, + 'lot_id': quant.lot_id.id, + 'package_id': quant.package_id.id, + 'location_id': quant.location_id.id, + 'owner_id': quant.owner_id.id, + } + if not (self.qty_done or vals.get('qty_done')): + line_data['qty_done'] = uom._compute_quantity(quant.available_quantity, quant.product_uom_id, rounding_method='HALF_UP') + return line_data + def action_open_reference(self): self.ensure_one() if self.move_id: @@ -937,14 +968,3 @@ class StockMoveLine(models.Model): 'message': _("The inventory adjustments have been reverted."), } } - - def action_open_reserve_stock(self): - move_id = self.env['stock.move'].browse(self.env.context.get('default_move_id')) - return { - 'type': 'ir.actions.act_window', - 'res_model': 'stock.quant.reserve', - 'view_mode': 'form', - 'context': {'default_move_id': move_id.id}, - 'target': 'new', - 'name': _('Reserve stock: %(product)s', product=move_id.product_id.name), - } diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 6746e5bb295..01adeba5f6c 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -523,6 +523,20 @@ class StockQuant(models.Model): 'target': 'new', } + def name_get(self): + """name that will be displayed in the detailed operation""" + name_parts = [] + for record in self: + name = [] + if self.env.user.has_group('stock.group_stock_multi_locations'): + name.append(record.location_id.display_name) + if self.env.user.has_group('stock.group_production_lot') and record.lot_id: + name.append(record.lot_id.name) + name_parts.append(name) + if name_parts: + return [(quant.id, ' - '.join(name)) for quant, name in zip(self, name_parts)] + return [] + @api.constrains('product_id') def check_product_id(self): if any(elem.product_id.type != 'product' for elem in self): diff --git a/addons/stock/static/src/fields/stock_move_line_x2_many_field.js b/addons/stock/static/src/fields/stock_move_line_x2_many_field.js new file mode 100644 index 00000000000..fa1e124b825 --- /dev/null +++ b/addons/stock/static/src/fields/stock_move_line_x2_many_field.js @@ -0,0 +1,47 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field"; +import { sprintf } from "@web/core/utils/strings"; +import { useSelectCreate } from "@web/views/fields/relational_utils"; +export class SMLX2ManyField extends X2ManyField { + setup() { + super.setup(); + const selectCreate = useSelectCreate({ + resModel: "stock.quant", + activeActions: this.activeActions, + onSelected: (resIds) => this.selectRecord(resIds), + onCreateEdit: ({ context }) => this._openRecord({ context }), + }); + + this.selectCreate = (params) => { + const p = Object.assign({}, params); + return selectCreate(p); + }; + } + + async onAdd({ context, editable } = {}) { + context = {}; + const { string } = this.props; + const title = sprintf(this.env._t("Add: %s"), string); + const domain = [ + ["product_id", "=", this.props.record.data.product_id[0]], + ["location_id", "child_of", this.props.context.default_location_id], + ]; + return this.selectCreate({ domain, context, title }); + } + + selectRecord(res_ids) { + const params = { + context: { default_quant_id: res_ids[0] }, + }; + this.addInLine(params); + } +} + +export const smlX2ManyField = { + ...x2ManyField, + component: SMLX2ManyField, +}; + +registry.category("fields").add("sml_x2_many", smlX2ManyField); diff --git a/addons/stock/static/src/widgets/stock_pick_from.js b/addons/stock/static/src/widgets/stock_pick_from.js new file mode 100644 index 00000000000..060204a5352 --- /dev/null +++ b/addons/stock/static/src/widgets/stock_pick_from.js @@ -0,0 +1,53 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Many2OneField, many2OneField } from "@web/views/fields/many2one/many2one_field"; + +const { onWillStart} = owl; + +export class StockPickFrom extends Many2OneField { + setup() { + super.setup(); + this.user = useService('user'); + onWillStart(async () => { + const testedGroup = [['location', 'stock.group_stock_multi_locations'], ['lot', 'stock.group_production_lot']]; + const userGroups = await Promise.all( + testedGroup.map((group) => this.user.hasGroup(group[1])) + ); + this.enabledGroups = {}; + for (const [index, group] of testedGroup.entries()) { + this.enabledGroups[group[0]] = userGroups[index]; + } + }); + } + get displayName() { + return super.displayName || this._quant_display_name(); + } + + get value() { + return super.value || [0, this._quant_display_name()]; + } + + _quant_display_name() { + let name_parts = []; + if (this.props.record.data.id) { + // if location group is activated + if (this.enabledGroups?.location) { + name_parts.push(this.props.record.data.location_id?.[1]) + } + if (this.enabledGroups?.lot) { + name_parts.push(this.props.record.data.lot_id?.[1] || this.props.record.data.lot_name) + } + return name_parts.join(" - "); + } + return ""; + } +} + +export const stockPickFrom = { + ...many2OneField, + component: StockPickFrom, +}; + +registry.category("fields").add("pick_from", stockPickFrom); diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 2ac3badae7a..96ebf8ad04b 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -157,6 +157,7 @@ + @@ -191,8 +192,11 @@ - -
+ +
@@ -222,10 +226,6 @@ 1000 - - -
+ +
+ + stock.quant.inventory.tree.editable diff --git a/addons/stock/wizard/stock_quant_reserve.py b/addons/stock/wizard/stock_quant_reserve.py deleted file mode 100644 index 7d3669014ee..00000000000 --- a/addons/stock/wizard/stock_quant_reserve.py +++ /dev/null @@ -1,68 +0,0 @@ -from odoo import _, api, Command, fields, models -from odoo.exceptions import UserError - - -class ReserveStockQuantLine(models.TransientModel): - _name = 'stock.quant.reserve.line' - _description = 'Choose quantity to reserve from each stock quant' - - quant_id = fields.Many2one('stock.quant', required=True, readonly=True) - location_id = fields.Many2one(related='quant_id.location_id') - product_id = fields.Many2one(related='quant_id.product_id') - product_uom_id = fields.Many2one(related='product_id.uom_id') - lot_id = fields.Many2one(related='quant_id.lot_id') - package_id = fields.Many2one(related='quant_id.package_id') - available_quantity = fields.Float(related='quant_id.available_quantity', string='Available') - qty_to_reserve = fields.Float('To reserve') - - reserve_id = fields.Many2one('stock.quant.reserve') - - -class ReserveStockQuant(models.TransientModel): - _name = 'stock.quant.reserve' - _description = 'Manually reserve stock' - - move_id = fields.Many2one('stock.move') - product_tracking = fields.Selection(related='move_id.has_tracking') - product_uom_id = fields.Many2one(related='move_id.product_id.uom_id', readonly=True) - demand_qty = fields.Float(compute='_compute_demand_qty', store=True) - qty_to_reserve = fields.Float('Quantity to reserve', compute='_compute_qty_to_reserve') - quant_line_ids = fields.One2many('stock.quant.reserve.line', 'reserve_id', compute='_compute_quant_line_ids', - readonly=False, store=True) - - @api.depends('quant_line_ids.qty_to_reserve', 'demand_qty') - def _compute_qty_to_reserve(self): - for wiz in self: - wiz.qty_to_reserve = max(wiz.demand_qty - sum(wiz.quant_line_ids.mapped('qty_to_reserve')), 0) - - @api.depends('move_id') - def _compute_demand_qty(self): - for wiz in self: - wiz.demand_qty = wiz.move_id.product_qty - sum(wiz.move_id.move_line_ids.mapped('reserved_qty')) - - @api.depends('move_id') - def _compute_quant_line_ids(self): - for wiz in self: - move_id = wiz.move_id - if not move_id: - wiz.quant_line_ids = False - continue - if wiz.quant_line_ids: - continue - quant_line_cmds = [Command.clear()] - quant_ids = self.env['stock.quant'].search([('product_id', '=', move_id.product_id.id), - ('location_id', 'child_of', move_id.location_id.id)]) - quant_line_cmds += [Command.create({'quant_id': quant.id}) for quant in quant_ids.filtered(lambda q: q.available_quantity > 0)] - wiz.quant_line_ids = quant_line_cmds - - def reserve_stock(self): - move_line_vals = [] - for wiz in self: - for line in wiz.quant_line_ids.filtered(lambda l: l.qty_to_reserve > 0): - if line.qty_to_reserve > line.available_quantity: - raise UserError(_('Cannot reserve more quantity than available!')) - line.quant_id.reserved_quantity += line.qty_to_reserve - move_line_vals.append(wiz.move_id._prepare_move_line_vals(quantity=line.qty_to_reserve, reserved_quant=line.quant_id)) - StockMoveLine = self.env['stock.move.line'].with_context(bypass_reservation_update=True) - StockMoveLine.create(move_line_vals) - self.move_id._recompute_state() diff --git a/addons/stock/wizard/stock_quant_reserve.xml b/addons/stock/wizard/stock_quant_reserve.xml deleted file mode 100644 index cefaa3c8d8b..00000000000 --- a/addons/stock/wizard/stock_quant_reserve.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Reserve stock - stock.quant.reserve - -
- - - - - - -
-

Choose from the following available stock and enter the quantity to reserve.

-
- - - - - - - - - - - - - - - -
-
-