diff --git a/addons/delivery/models/stock_move.py b/addons/delivery/models/stock_move.py index 691c1c3dd42d8598ecee45afcc8eb696a39a0d8f..805aaf5bfa712e2a24331d31e64c8a8a734a6461 100644 --- a/addons/delivery/models/stock_move.py +++ b/addons/delivery/models/stock_move.py @@ -15,3 +15,12 @@ class StockMove(models.Model): def _cal_move_weight(self): for move in self.filtered(lambda moves: moves.product_id.weight > 0.00): move.weight = (move.product_qty * move.product_id.weight) + + def _get_new_picking_values(self): + vals = super(StockMove, self)._get_new_picking_values() + vals['carrier_id'] = self.mapped('sale_line_id.order_id.carrier_id').id + return vals + + def _key_assign_picking(self): + keys = super(StockMove, self)._key_assign_picking() + return keys + (self.sale_line_id.order_id.carrier_id,) diff --git a/addons/procurement_jit/sale.py b/addons/procurement_jit/sale.py index 9ad3cf50b1de28fc6ae290e4bc0c338992093a3a..c6e638ee721bce3b98056fef3d71eda22d02c8ab 100644 --- a/addons/procurement_jit/sale.py +++ b/addons/procurement_jit/sale.py @@ -8,8 +8,8 @@ class SaleOrderLine(models.Model): _inherit = "sale.order.line" @api.multi - def _action_launch_stock_rule(self): - res = super(SaleOrderLine, self)._action_launch_stock_rule() + def _action_launch_stock_rule(self, previous_product_uom_qty=False): + res = super(SaleOrderLine, self)._action_launch_stock_rule(previous_product_uom_qty=previous_product_uom_qty) orders = list(set(x.order_id for x in self)) for order in orders: reassign = order.picking_ids.filtered(lambda x: x.state=='confirmed' or (x.state in ['waiting', 'assigned'] and not x.printed)) diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py index 144a1c93a86ccaaf425d120c01821dc0b00649bc..d80fdf068a777afcbdeed85981041d639338a5bb 100644 --- a/addons/sale_mrp/models/sale_mrp.py +++ b/addons/sale_mrp/models/sale_mrp.py @@ -91,7 +91,7 @@ class SaleOrderLine(models.Model): warning = {'warning': warning_mess} return warning - def _get_qty_procurement(self): + def _get_qty_procurement(self, previous_product_uom_qty=False): self.ensure_one() # Specific case when we change the qty on a SO for a kit product. # We don't try to be too smart and keep a simple approach: we compare the quantity before @@ -99,8 +99,8 @@ class SaleOrderLine(models.Model): # sent, or any other exceptional case. bom = self.env['mrp.bom']._bom_find(product=self.product_id, bom_type='phantom') if bom and 'previous_product_uom_qty' in self.env.context: - return self.env.context['previous_product_uom_qty'].get(self.id, 0.0) - return super(SaleOrderLine, self)._get_qty_procurement() + return previous_product_uom_qty and previous_product_uom_qty.get(self.id, 0.0) + return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty) class AccountInvoiceLine(models.Model): diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 45974190d631587174574ec36e35107abf66db26..fc636b8177a5cf99b391ad9a6bbe188100eb3d83 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -60,17 +60,24 @@ class SaleOrder(models.Model): def write(self, values): if values.get('order_line') and self.state == 'sale': for order in self: - pre_order_line_qty = {order_line: order_line.product_uom_qty for order_line in order.mapped('order_line')} + pre_order_line_qty = {order_line: order_line.product_uom_qty for order_line in order.mapped('order_line') if not order_line.is_expense} res = super(SaleOrder, self).write(values) if values.get('order_line') and self.state == 'sale': for order in self: to_log = {} + order_lines_to_run = self.env['sale.order.line'] for order_line in order.order_line: - if pre_order_line_qty.get(order_line, False) and float_compare(order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) < 0: + if order_line not in pre_order_line_qty: + order_lines_to_run |= order_line + elif float_compare(order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) < 0: to_log[order_line] = (order_line.product_uom_qty, pre_order_line_qty[order_line]) + elif float_compare(order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) > 0: + order_lines_to_run |= order_line if to_log: documents = self.env['stock.picking']._log_activity_get_documents(to_log, 'move_ids', 'UP') order._log_decrease_ordered_quantity(documents) + if order_lines_to_run: + order_lines_to_run._action_launch_stock_rule(pre_order_line_qty) return res @api.multi @@ -155,7 +162,6 @@ class SaleOrder(models.Model): self.env['stock.picking']._log_activity(_render_note_exception_quantity_so, documents) - class SaleOrderLine(models.Model): _inherit = 'sale.order.line' @@ -199,19 +205,6 @@ class SaleOrderLine(models.Model): lines.filtered(lambda line: line.state == 'sale')._action_launch_stock_rule() return lines - @api.multi - def write(self, values): - lines = self.env['sale.order.line'] - if 'product_uom_qty' in values: - precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - lines = self.filtered( - lambda r: r.state == 'sale' and not r.is_expense and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) - previous_product_uom_qty = {line.id: line.product_uom_qty for line in lines} - res = super(SaleOrderLine, self).write(values) - if lines: - lines.with_context(previous_product_uom_qty=previous_product_uom_qty)._action_launch_stock_rule() - return res - @api.depends('order_id.state') def _compute_invoice_status(self): super(SaleOrderLine, self)._compute_invoice_status() @@ -341,7 +334,7 @@ class SaleOrderLine(models.Model): }) return values - def _get_qty_procurement(self): + def _get_qty_procurement(self, previous_product_uom_qty=False): self.ensure_one() qty = 0.0 for move in self.move_ids.filtered(lambda r: r.state != 'cancel'): @@ -352,7 +345,7 @@ class SaleOrderLine(models.Model): return qty @api.multi - def _action_launch_stock_rule(self): + def _action_launch_stock_rule(self, previous_product_uom_qty=False): """ Launch procurement group run method with required/custom fields genrated by a sale order line. procurement group will launch '_run_pull', '_run_buy' or '_run_manufacture' @@ -363,7 +356,7 @@ class SaleOrderLine(models.Model): for line in self: if line.state != 'sale' or not line.product_id.type in ('consu','product'): continue - qty = line._get_qty_procurement() + qty = line._get_qty_procurement(previous_product_uom_qty) if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index f622c61820e102e1e654ba73a64aa5f7d35a2988..36c2795e61dd3649fe99fa48d4e6219be4afc154 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -38,11 +38,14 @@ class StockMove(models.Model): def _assign_picking_post_process(self, new=False): super(StockMove, self)._assign_picking_post_process(new=new) - if new and self.sale_line_id and self.sale_line_id.order_id: - self.picking_id.message_post_with_view( - 'mail.message_origin_link', - values={'self': self.picking_id, 'origin': self.sale_line_id.order_id}, - subtype_id=self.env.ref('mail.mt_note').id) + if new: + picking_id = self.mapped('picking_id') + sale_order_ids = self.mapped('sale_line_id.order_id') + for sale_order_id in sale_order_ids: + picking_id.message_post_with_view( + 'mail.message_origin_link', + values={'self': picking_id, 'origin': sale_order_id}, + subtype_id=self.env.ref('mail.mt_note').id) class ProcurementGroup(models.Model): diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index 0be82e262d0a10f6b7dddfb308134df47cac9db2..7782ecdc7f1ca6a7569b9af7126b1734a23b88ba 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -396,14 +396,22 @@ class TestSaleStock(TestSale): self.assertEqual(move1.product_qty, 12) # edit the so line, sell 2 dozen, the move should now be 24 units - so1.order_line.product_uom_qty = 2 + so1.write({ + 'order_line': [ + (1, so1.order_line.id, {'product_uom_qty': 2}), + ] + }) self.assertEqual(move1.product_uom_qty, 24) self.assertEqual(move1.product_uom.id, uom_unit.id) self.assertEqual(move1.product_qty, 24) # force the propagation of the uom, sell 3 dozen self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1') - so1.order_line.product_uom_qty = 3 + so1.write({ + 'order_line': [ + (1, so1.order_line.id, {'product_uom_qty': 3}), + ] + }) move2 = so1.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id) self.assertEqual(move2.product_uom_qty, 1) self.assertEqual(move2.product_uom.id, uom_dozen.id) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index d1104bdb08488b8958c91be81f6fa37ec06d00a7..1a6eff0447b4e20797eedfed8442f662171d087d 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -675,6 +675,10 @@ class StockMove(models.Model): } } + def _key_assign_picking(self): + self.ensure_one() + return self.group_id, self.location_id, self.location_dest_id, self.picking_type_id + def _search_picking_for_assignation(self): self.ensure_one() picking = self.env['stock.picking'].search([ @@ -692,11 +696,16 @@ class StockMove(models.Model): type (moves should already have them identical). Otherwise, create a new picking to assign them to. """ Picking = self.env['stock.picking'] - for move in self: - recompute = False - picking = move._search_picking_for_assignation() + grouped_moves = groupby(sorted(self, key=lambda m: [f.id for f in m._key_assign_picking()]), key=lambda m: [m._key_assign_picking()]) + for group, moves in grouped_moves: + moves = self.env['stock.move'].concat(*list(moves)) + new_picking = False + # Could pass the arguments contained in group but they are the same + # for each move that why moves[0] is acceptable + picking = moves[0]._search_picking_for_assignation() if picking: - if picking.partner_id.id != move.partner_id.id or picking.origin != move.origin: + if any(picking.partner_id.id != m.partner_id.id or + picking.origin != m.origin for m in moves): # If a picking is found, we'll append `move` to its move list and thus its # `partner_id` and `ref` field will refer to multiple records. In this # case, we chose to wipe them. @@ -705,34 +714,32 @@ class StockMove(models.Model): 'origin': False, }) else: - recompute = True - picking = Picking.create(move._get_new_picking_values()) - - move.write({'picking_id': picking.id}) - move._assign_picking_post_process(new=recompute) - # If this method is called in batch by a write on a one2many and - # at some point had to create a picking, some next iterations could - # try to find back the created picking. As we look for it by searching - # on some computed fields, we have to force a recompute, else the - # record won't be found. - if recompute: - move.recompute() + new_picking = True + picking = Picking.create(moves._get_new_picking_values()) + + moves.write({'picking_id': picking.id}) + moves._assign_picking_post_process(new=new_picking) return True def _assign_picking_post_process(self, new=False): pass def _get_new_picking_values(self): - """ Prepares a new picking for this move as it could not be assigned to - another picking. This method is designed to be inherited. """ + """ return create values for new picking that will be linked with group + of moves in self. + """ + origins = self.filtered(lambda m: m.origin).mapped('origin') + origin = len(origins) == 1 and origins[0] or False + partners = self.mapped('partner_id') + partner = len(partners) == 1 and partners.id or False return { - 'origin': self.origin, - 'company_id': self.company_id.id, - 'move_type': self.group_id and self.group_id.move_type or 'direct', - 'partner_id': self.partner_id.id, - 'picking_type_id': self.picking_type_id.id, - 'location_id': self.location_id.id, - 'location_dest_id': self.location_dest_id.id, + 'origin': origin, + 'company_id': self.mapped('company_id').id, + 'move_type': self.mapped('group_id').move_type or 'direct', + 'partner_id': partner, + 'picking_type_id': self.mapped('picking_type_id').id, + 'location_id': self.mapped('location_id').id, + 'location_dest_id': self.mapped('location_dest_id').id, } def _should_be_assigned(self): diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py index b527701a04985b55f9aff551d13df0d245b7a7dc..5e23612a0981fbb46a68e8599b77d9834b9e197d 100644 --- a/addons/stock_dropshipping/models/sale.py +++ b/addons/stock_dropshipping/models/sale.py @@ -10,7 +10,7 @@ class SaleOrderLine(models.Model): purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id') @api.multi - def _get_qty_procurement(self): + def _get_qty_procurement(self, previous_product_uom_qty): # People without purchase rights should be able to do this operation purchase_lines_sudo = self.sudo().purchase_line_ids if not self.move_ids.filtered(lambda r: r.state != 'cancel') and purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'): @@ -19,5 +19,4 @@ class SaleOrderLine(models.Model): qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP') return qty else: - return super(SaleOrderLine, self)._get_qty_procurement() - + return super(SaleOrderLine, self)._get_qty_procurement(previous_product_uom_qty=previous_product_uom_qty)