diff --git a/addons/product/models/product_packaging.py b/addons/product/models/product_packaging.py index d62f1bd7f5b..f17738b38df 100644 --- a/addons/product/models/product_packaging.py +++ b/addons/product/models/product_packaging.py @@ -64,3 +64,17 @@ class ProductPackaging(models.Model): if new_qty == product_qty: return packaging return self.env['product.packaging'] + + def _compute_qty(self, qty, qty_uom=False): + """Returns the qty of this packaging that qty converts to. + A float is returned because there are edge cases where some users use + "part" of a packaging + + :param qty: float of product quantity (given in product UoM if no qty_uom provided) + :param qty_uom: Optional uom of quantity + :returns: float of packaging qty + """ + self.ensure_one() + if qty_uom: + qty = qty_uom._compute_quantity(qty, self.product_uom_id) + return float_round(qty / self.qty, precision_rounding=self.product_uom_id.rounding) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 4ef0a12c735..8a097d20a69 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -1298,13 +1298,11 @@ class PurchaseOrderLine(models.Model): @api.depends('product_packaging_id', 'product_uom', 'product_qty') def _compute_product_packaging_qty(self): + self.product_packaging_qty = 0 for line in self: if not line.product_packaging_id: - line.product_packaging_qty = 0 - else: - packaging_uom = line.product_packaging_id.product_uom_id - packaging_uom_qty = line.product_uom._compute_quantity(line.product_qty, packaging_uom) - line.product_packaging_qty = float_round(packaging_uom_qty / line.product_packaging_id.qty, precision_rounding=packaging_uom.rounding) + continue + line.product_packaging_qty = line.product_packaging_id._compute_qty(line.product_qty, line.product_uom) @api.depends('product_packaging_qty') def _compute_product_qty(self): diff --git a/addons/purchase/report/purchase_order_templates.xml b/addons/purchase/report/purchase_order_templates.xml index 18cc431fb3c..fc73e76014f 100644 --- a/addons/purchase/report/purchase_order_templates.xml +++ b/addons/purchase/report/purchase_order_templates.xml @@ -81,6 +81,9 @@ + + ( ) + diff --git a/addons/purchase/report/purchase_quotation_templates.xml b/addons/purchase/report/purchase_quotation_templates.xml index cbecae0bf61..48f339af487 100644 --- a/addons/purchase/report/purchase_quotation_templates.xml +++ b/addons/purchase/report/purchase_quotation_templates.xml @@ -41,6 +41,9 @@ + + ( ) + diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index e4d85f38758..d5b1187a950 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -649,15 +649,11 @@ class SaleOrderLine(models.Model): @api.depends('product_packaging_id', 'product_uom', 'product_uom_qty') def _compute_product_packaging_qty(self): + self.product_packaging_qty = 0 for line in self: if not line.product_packaging_id: - line.product_packaging_qty = False - else: - packaging_uom = line.product_packaging_id.product_uom_id - packaging_uom_qty = line.product_uom._compute_quantity(line.product_uom_qty, packaging_uom) - line.product_packaging_qty = float_round( - packaging_uom_qty / line.product_packaging_id.qty, - precision_rounding=packaging_uom.rounding) + continue + line.product_packaging_qty = line.product_packaging_id._compute_qty(line.product_uom_qty, line.product_uom) # This computed default is necessary to have a clean computation inheritance # (cf sale_stock) instead of simply removing the default and specifying diff --git a/addons/sale/report/ir_actions_report_templates.xml b/addons/sale/report/ir_actions_report_templates.xml index 1fb4515fa7b..86502b8adef 100644 --- a/addons/sale/report/ir_actions_report_templates.xml +++ b/addons/sale/report/ir_actions_report_templates.xml @@ -104,6 +104,9 @@ 3 units + + ( ) + 3 diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 097409324ca..8d6338f82cb 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -182,6 +182,12 @@ class StockMove(models.Model): reservation_date = fields.Date('Date to Reserve', compute='_compute_reservation_date', store=True, help="Computes when a move should be reserved") product_packaging_id = fields.Many2one('product.packaging', 'Packaging', domain="[('product_id', '=', product_id)]", check_company=True) + product_packaging_qty = fields.Float( + string="Reserved Packaging Quantity", + compute='_compute_product_packaging_qty') + product_packaging_qty_done = fields.Float( + string="Done Packaging Quantity", + compute='_compute_product_packaging_qty_done') from_immediate_transfer = fields.Boolean(related="picking_id.immediate_transfer") show_reserved = fields.Boolean(compute='_compute_show_reserved') show_quant = fields.Boolean("Show Quant", compute="_compute_show_info") @@ -293,6 +299,22 @@ class StockMove(models.Model): for move in self.filtered(lambda m: m.picking_id): move.partner_id = move.picking_id.partner_id + @api.depends('product_packaging_id', 'product_uom', 'product_qty') + def _compute_product_packaging_qty(self): + self.product_packaging_qty = False + for move in self: + if not move.product_packaging_id: + continue + move.product_packaging_qty = move.product_packaging_id._compute_qty(move.product_qty) + + @api.depends('product_packaging_id', 'product_uom', 'quantity_done') + def _compute_product_packaging_qty_done(self): + self.product_packaging_qty_done = False + for move in self: + if not move.product_packaging_id: + continue + move.product_packaging_qty_done = move.product_packaging_id._compute_qty(move.quantity_done, move.product_uom) + def _get_move_lines(self): """ This will return the move lines to consider when applying _quantity_done_compute on a stock.move. In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line.""" diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 2016b6c7360..cea0b0ae1fe 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -85,6 +85,12 @@ class StockMoveLine(models.Model): 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 + product_packaging_qty = fields.Float( + string="Reserved Packaging Quantity", + compute='_compute_product_packaging_qty') + product_packaging_qty_done = fields.Float( + string="Done Packaging Quantity", + compute='_compute_product_packaging_qty_done') @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): @@ -119,6 +125,22 @@ class StockMoveLine(models.Model): if not line.location_dest_id: line.location_dest_id = line.move_id.location_dest_id or line.picking_id.location_dest_id + @api.depends('move_id.product_packaging_id', 'product_uom_id', 'reserved_qty') + def _compute_product_packaging_qty(self): + self.product_packaging_qty = 0 + for line in self: + if not line.move_id.product_packaging_id: + continue + line.product_packaging_qty = line.move_id.product_packaging_id._compute_qty(line.reserved_qty) + + @api.depends('move_id.product_packaging_id', 'product_uom_id', 'qty_done') + def _compute_product_packaging_qty_done(self): + self.product_packaging_qty_done = 0 + for line in self: + if not line.move_id.product_packaging_id: + continue + line.product_packaging_qty_done = line.move_id.product_packaging_id._compute_qty(line.qty_done, line.product_uom_id) + def _search_picking_type_id(self, operator, value): return [('picking_id.picking_type_id', operator, value)] @@ -799,14 +821,14 @@ class StockMoveLine(models.Model): move_to_recompute_state._recompute_state() def _get_aggregated_product_quantities(self, **kwargs): - """ Returns a dictionary of products (key = id+name+description+uom) and corresponding values of interest. + """ Returns a dictionary of products (key = id+name+description+uom+packaging) and corresponding values of interest. Allows aggregation of data across separate move lines for the same product. This is expected to be useful in things such as delivery reports. Dict key is made as a combination of values we expect to want to group the products by (i.e. so data is not lost). This function purposely ignores lots/SNs because these are expected to already be properly grouped by line. - returns: dictionary {product_id+name+description+uom: {product, name, description, qty_done, product_uom}, ...} + returns: dictionary {product_id+name+description+uom+packaging: {product, name, description, qty_done, product_uom, packaging}, ...} """ aggregated_move_lines = {} @@ -818,8 +840,16 @@ class StockMoveLine(models.Model): if description == name or description == move.product_id.name: description = False product = move.product_id - line_key = f'{product.id}_{product.display_name}_{description or ""}_{uom.id}' - return (line_key, name, description, uom) + line_key = f'{product.id}_{product.display_name}_{description or ""}_{uom.id}_{move.product_packaging_id or ""}' + return (line_key, name, description, uom, move.product_packaging_id) + + def _compute_packaging_qtys(aggregated_move_lines): + # Needs to be computed after aggregation of line qtys + for line in aggregated_move_lines.values(): + if line['packaging']: + line['packaging_qty'] = line['packaging']._compute_qty(line['qty_ordered'], line['product_uom']) + line['packaging_qty_done'] = line['packaging']._compute_qty(line['qty_done'], line['product_uom']) + return aggregated_move_lines # Loops to get backorders, backorders' backorders, and so and so... backorders = self.env['stock.picking'] @@ -831,8 +861,7 @@ class StockMoveLine(models.Model): for move_line in self: if kwargs.get('except_package') and move_line.result_package_id: continue - line_key, name, description, uom = get_aggregated_properties(move_line=move_line) - + line_key, name, description, uom, packaging = get_aggregated_properties(move_line=move_line) qty_done = move_line.product_uom_id._compute_quantity(move_line.qty_done, uom) if line_key not in aggregated_move_lines: qty_ordered = None @@ -849,12 +878,15 @@ class StockMoveLine(models.Model): lambda ml: get_aggregated_properties(move=ml.move_id)[0] == line_key and ml.id != move_line.id ) qty_ordered -= sum(map(lambda m: m.product_uom_id._compute_quantity(m.qty_done, uom), previous_move_lines)) - aggregated_move_lines[line_key] = {'name': name, - 'description': description, - 'qty_done': qty_done, - 'qty_ordered': qty_ordered or qty_done, - 'product_uom': uom, - 'product': move_line.product_id} + aggregated_move_lines[line_key] = { + 'name': name, + 'description': description, + 'qty_done': qty_done, + 'qty_ordered': qty_ordered or qty_done, + 'product_uom': uom, + 'product': move_line.product_id, + 'packaging': packaging, + } else: aggregated_move_lines[line_key]['qty_ordered'] += qty_done aggregated_move_lines[line_key]['qty_done'] += qty_done @@ -862,13 +894,13 @@ class StockMoveLine(models.Model): # Does the same for empty move line to retrieve the ordered qty. for partially done moves # (as they are splitted when the transfer is done and empty moves don't have move lines). if kwargs.get('strict'): - return aggregated_move_lines + return _compute_packaging_qtys(aggregated_move_lines) pickings = (self.picking_id | backorders) for empty_move in pickings.move_ids: if not (empty_move.state == "cancel" and empty_move.product_uom_qty and float_is_zero(empty_move.quantity_done, precision_rounding=empty_move.product_uom.rounding)): continue - line_key, name, description, uom = get_aggregated_properties(move=empty_move) + line_key, name, description, uom, packaging = get_aggregated_properties(move=empty_move) if line_key not in aggregated_move_lines: qty_ordered = empty_move.product_uom_qty @@ -879,11 +911,12 @@ class StockMoveLine(models.Model): 'qty_ordered': qty_ordered, 'product_uom': uom, 'product': empty_move.product_id, + 'packaging': packaging, } else: aggregated_move_lines[line_key]['qty_ordered'] += empty_move.product_uom_qty - return aggregated_move_lines + return _compute_packaging_qtys(aggregated_move_lines) def _compute_sale_price(self): # To Override diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index eb779048d1a..e1e8bc8fbf5 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -78,10 +78,16 @@ 3.00 units + + ( ) + 3.00 units + + ( ) + @@ -240,12 +246,18 @@ + + ( ) + + + ( ) + diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml index 9c01255daf5..a2964153835 100644 --- a/addons/stock/report/report_stockpicking_operations.xml +++ b/addons/stock/report/report_stockpicking_operations.xml @@ -118,6 +118,14 @@ 3.00 3.00 units + + + ( ) + + + ( ) + + WH/Stock diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index 2b6c94c5634..e39cfbfa838 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -5660,8 +5660,8 @@ class StockMove(TransactionCase): self.assertEqual(len(aggregate_values), 2) sml1 = delivery.move_line_ids.filtered(lambda ml: ml.product_id == self.product) sml2 = delivery.move_line_ids.filtered(lambda ml: ml.product_id == product2) - aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}'] - aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}'] + aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}_'] + aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}_'] self.assertEqual(aggregate_val_1['qty_ordered'], 10) self.assertEqual(aggregate_val_1['qty_done'], 6) self.assertEqual(aggregate_val_2['qty_ordered'], 10) @@ -5682,8 +5682,8 @@ class StockMove(TransactionCase): self.assertEqual(len(aggregate_values), 2) sml1 = delivery.move_line_ids.filtered(lambda ml: ml.product_id == self.product) sml2 = delivery.move_line_ids.filtered(lambda ml: ml.product_id == product2) - aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}'] - aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}'] + aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}_'] + aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}_'] self.assertEqual(aggregate_val_1['qty_ordered'], 10) self.assertEqual(aggregate_val_1['qty_done'], 6) self.assertEqual(aggregate_val_2['qty_ordered'], 10) @@ -5694,9 +5694,9 @@ class StockMove(TransactionCase): sml1 = first_backorder.move_line_ids.filtered(lambda ml: ml.product_id == self.product) sml2 = first_backorder.move_line_ids.filtered(lambda ml: ml.product_id == product2) sml3 = first_backorder.move_line_ids.filtered(lambda ml: ml.product_id == product3) - aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}'] - aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}'] - aggregate_val_3 = aggregate_values[f'{product3.id}_{product3.name}__{sml3.product_uom_id.id}'] + aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}_'] + aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}_'] + aggregate_val_3 = aggregate_values[f'{product3.id}_{product3.name}__{sml3.product_uom_id.id}_'] self.assertEqual(aggregate_val_1['qty_ordered'], 4) self.assertEqual(aggregate_val_1['qty_done'], 4) self.assertEqual(aggregate_val_2['qty_ordered'], 8) @@ -5715,8 +5715,8 @@ class StockMove(TransactionCase): self.assertEqual(len(aggregate_values), 2) sml1 = delivery.move_line_ids.filtered(lambda ml: ml.product_id == self.product) sml2 = delivery.move_line_ids.filtered(lambda ml: ml.product_id == product2) - aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}'] - aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}'] + aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}_'] + aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}_'] self.assertEqual(aggregate_val_1['qty_ordered'], 10) self.assertEqual(aggregate_val_1['qty_done'], 6) self.assertEqual(aggregate_val_2['qty_ordered'], 10) @@ -5727,9 +5727,9 @@ class StockMove(TransactionCase): sml1 = first_backorder.move_line_ids.filtered(lambda ml: ml.product_id == self.product) sml2 = first_backorder.move_line_ids.filtered(lambda ml: ml.product_id == product2) sml3 = first_backorder.move_line_ids.filtered(lambda ml: ml.product_id == product3) - aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}'] - aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}'] - aggregate_val_3 = aggregate_values[f'{product3.id}_{product3.name}__{sml3.product_uom_id.id}'] + aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{sml1.product_uom_id.id}_'] + aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sml2.product_uom_id.id}_'] + aggregate_val_3 = aggregate_values[f'{product3.id}_{product3.name}__{sml3.product_uom_id.id}_'] self.assertEqual(aggregate_val_1['qty_ordered'], 4) self.assertEqual(aggregate_val_1['qty_done'], 4) self.assertEqual(aggregate_val_2['qty_ordered'], 8) @@ -5741,8 +5741,8 @@ class StockMove(TransactionCase): self.assertEqual(len(aggregate_values), 2) sml1 = second_backorder.move_line_ids.filtered(lambda ml: ml.product_id == product3) sm2 = second_backorder.move_ids.filtered(lambda ml: ml.product_id == product2) - aggregate_val_1 = aggregate_values[f'{product3.id}_{product3.name}__{sml1.product_uom_id.id}'] - aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sm2.product_uom.id}'] + aggregate_val_1 = aggregate_values[f'{product3.id}_{product3.name}__{sml1.product_uom_id.id}_'] + aggregate_val_2 = aggregate_values[f'{product2.id}_{product2.name}__{sm2.product_uom.id}_'] self.assertEqual(aggregate_val_1['qty_ordered'], 3) self.assertEqual(aggregate_val_1['qty_done'], 3) self.assertEqual(aggregate_val_2['qty_ordered'], 2) @@ -5799,7 +5799,7 @@ class StockMove(TransactionCase): 'location_dest_id': move2.location_dest_id.id, }) aggregate_values = picking.move_line_ids._get_aggregated_product_quantities() - aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}'] + aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] self.assertEqual(aggregated_val['qty_ordered'], 15) picking.button_validate() self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 10) @@ -5844,11 +5844,11 @@ class StockMove(TransactionCase): self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 15) aggregate_values1 = picking.move_line_ids[0]._get_aggregated_product_quantities(strict=True) - aggregated_val = aggregate_values1[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}'] + aggregated_val = aggregate_values1[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] self.assertEqual(aggregated_val['qty_ordered'], 10) aggregate_values2 = picking.move_line_ids[1]._get_aggregated_product_quantities(strict=True) - aggregated_val = aggregate_values2[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}'] + aggregated_val = aggregate_values2[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] self.assertEqual(aggregated_val['qty_ordered'], 5) def test_move_line_aggregated_product_quantities_incomplete_package(self): @@ -5886,23 +5886,96 @@ class StockMove(TransactionCase): picking.backorder_ids.action_cancel() aggregate_values = picking.move_line_ids._get_aggregated_product_quantities() - aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}'] + aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] self.assertEqual(aggregated_val['qty_ordered'], 15) self.assertEqual(aggregated_val['qty_done'], 5) aggregate_values = picking.move_line_ids._get_aggregated_product_quantities(strict=True) - aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}'] + aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] self.assertEqual(aggregated_val['qty_ordered'], 5) self.assertEqual(aggregated_val['qty_done'], 5) aggregate_values = picking.move_line_ids._get_aggregated_product_quantities(except_package=True) - aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}'] + aggregated_val = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] self.assertEqual(aggregated_val['qty_ordered'], 10) self.assertEqual(aggregated_val['qty_done'], False) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.stock_location), 20) self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product, self.customer_location), 5) + def test_move_line_aggregated_product_quantities_packagings(self): + """ Test the `stock.move.line` method `_get_aggregated_product_quantities`, + which returns data used to print delivery slips, with product packagings + """ + self.env.user.groups_id += self.env.ref("product.group_stock_packaging") + packaging_of_4 = self.env['product.packaging'].create({ + 'name': 'pack of 4', + 'product_id': self.product.id, + 'qty': 4 + }) + packaging_of_5 = self.env['product.packaging'].create({ + 'name': 'pack of 5', + 'product_id': self.product.id, + 'qty': 5 + }) + packaging_of_2_dozen = self.env['product.packaging'].create({ + 'name': 'pack of 2 dozen', + 'product_id': self.product.id, + 'qty': 24, + }) + self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 25) + delivery_form = self.env['stock.picking'].create({ + 'state': 'draft', + 'immediate_transfer': False, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, + }) + delivery_form = Form(delivery_form) + with delivery_form.move_ids_without_package.new() as move: + move.product_id = self.product + move.product_uom_qty = 4 + with delivery_form.move_ids_without_package.new() as move: + move.product_id = self.product + move.product_uom_qty = 10 + with delivery_form.move_ids_without_package.new() as move: + move.product_id = self.product + move.product_uom = self.uom_dozen + move.product_uom_qty = 2 + with delivery_form.move_ids_without_package.new() as move: + move.product_id = self.product + move.product_uom_qty = 3 + delivery = delivery_form.save() + delivery.action_assign() + + self.assertEqual(delivery.move_ids_without_package[0].product_packaging_id, packaging_of_4) + self.assertEqual(delivery.move_ids_without_package[1].product_packaging_id, packaging_of_5) + self.assertEqual(delivery.move_ids_without_package[2].product_packaging_id, packaging_of_2_dozen) + self.assertFalse(delivery.move_ids_without_package[3].product_packaging_id) + + for move in delivery.move_ids_without_package: + move.quantity_done = move.product_uom_qty + aggregate_values = delivery.move_line_ids._get_aggregated_product_quantities() + self.assertEqual(len(aggregate_values), 4, "Each packaging should have their own line") + aggregate_val_1 = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_{packaging_of_4}'] + aggregate_val_2 = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_{packaging_of_5}'] + aggregate_val_3 = aggregate_values[f'{self.product.id}_{self.product.name}__{self.uom_dozen.id}_{packaging_of_2_dozen}'] + aggregate_val_4 = aggregate_values[f'{self.product.id}_{self.product.name}__{self.product.uom_id.id}_'] + self.assertEqual(aggregate_val_1['qty_ordered'], 4) + self.assertEqual(aggregate_val_1['qty_done'], 4) + self.assertEqual(aggregate_val_1['packaging_qty'], 1) + self.assertEqual(aggregate_val_1['packaging_qty_done'], 1) + self.assertEqual(aggregate_val_2['qty_ordered'], 10) + self.assertEqual(aggregate_val_2['qty_done'], 10) + self.assertEqual(aggregate_val_2['packaging_qty'], 2) + self.assertEqual(aggregate_val_2['packaging_qty_done'], 2) + self.assertEqual(aggregate_val_3['qty_ordered'], 2) + self.assertEqual(aggregate_val_3['qty_done'], 2) + self.assertEqual(aggregate_val_3['packaging_qty'], 1) + self.assertEqual(aggregate_val_3['packaging_qty_done'], 1) + self.assertEqual(aggregate_val_4['qty_ordered'], 3) + self.assertEqual(aggregate_val_4['qty_done'], 3) + self.assertFalse(aggregate_val_4.get('packaging_qty')) + self.assertFalse(aggregate_val_4.get('packaging_qty_done')) + def test_move_sn_warning(self): """ Check that warnings pop up when duplicate SNs added or when SN isn't in expected location.