[IMP] purchase, sale, stock: add packaging in reports

Several reports don't include product packing info on them. This is
useful for customers to see that what they bought matches what they
expected and for pickers who need to know which product packaging they
should be selecting from stock. Reports this was added to are:
- Sales orders,
- purchase orders (including RFQs),
- picking operations,
- delivery slips.

Note that we purposely exclude backordered moves since they weren't
deemed to be necessary at the time this feature was created.

Also includes light refactoring to remove repeated code for conversion
of product qty to packaging qty and to make it easier to do this
conversion in the future (i.e. can pass product.packaging without
original record that it was assigned to)

Task id 2927379

closes odoo/odoo#96858

Signed-off-by: Steve Van Essche <svs@odoo.com>
This commit is contained in:
Triet Ngo
2023-10-02 14:12:45 +00:00
committed by Tiffany Chang (tic)
parent f2ea69a8cf
commit d05372eb7b
11 changed files with 212 additions and 47 deletions
@@ -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)
+3 -5
View File
@@ -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):
@@ -81,6 +81,9 @@
<td class="text-end">
<span t-field="line.product_qty"/>
<span t-field="line.product_uom.name" groups="uom.group_uom"/>
<span t-if="line.product_packaging_id">
(<span t-field="line.product_packaging_qty" t-options='{"widget": "integer"}'/> <span t-field="line.product_packaging_id"/>)
</span>
</td>
<td class="text-end">
<span t-field="line.price_unit"/>
@@ -41,6 +41,9 @@
<td class="text-end">
<span t-field="order_line.product_qty"/>
<span t-field="order_line.product_uom" groups="uom.group_uom"/>
<span t-if="order_line.product_packaging_id">
(<span t-field="order_line.product_packaging_qty" t-options='{"widget": "integer"}'/> <span t-field="order_line.product_packaging_id"/>)
</span>
</td>
</t>
<t t-else="">
+3 -7
View File
@@ -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
@@ -104,6 +104,9 @@
<td name="td_quantity" class="text-end">
<span t-field="line.product_uom_qty">3</span>
<span t-field="line.product_uom">units</span>
<span t-if="line.product_packaging_id">
(<span t-field="line.product_packaging_qty" t-options='{"widget": "integer"}'/> <span t-field="line.product_packaging_id"/>)
</span>
</td>
<td name="td_priceunit" class="text-end">
<span t-field="line.price_unit">3</span>
+22
View File
@@ -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."""
+48 -15
View File
@@ -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
@@ -78,10 +78,16 @@
<td>
<span t-field="move.product_uom_qty">3.00</span>
<span t-field="move.product_uom">units</span>
<span t-if="move.product_packaging_id">
(<span t-field="move.product_packaging_qty" t-options='{"widget": "integer"}'/> <span t-field="move.product_packaging_id"/>)
</span>
</td>
<td>
<span t-field="move.quantity_done">3.00</span>
<span t-field="move.product_uom">units</span>
<span t-if="move.product_packaging_id">
(<span t-field="move.product_packaging_qty_done" t-options='{"widget": "integer"}'/> <span t-field="move.product_packaging_id"/>)
</span>
</td>
</tr>
</tbody>
@@ -240,12 +246,18 @@
<span t-esc="aggregated_lines[line]['qty_ordered']"
t-options="{'widget': 'float', 'decimal_precision': 'Product Unit of Measure'}"/>
<span t-esc="aggregated_lines[line]['product_uom'].name"/>
<span t-if="aggregated_lines[line]['packaging'].name">
(<span t-out="aggregated_lines[line]['packaging_qty']" t-options='{"widget": "integer"}'/> <span t-out="aggregated_lines[line]['packaging'].name"/>)
</span>
</td>
<td class="text-center" name="move_line_aggregated_qty_done">
<t t-if="aggregated_lines[line]['qty_done']">
<span t-esc="aggregated_lines[line]['qty_done']"
t-options="{'widget': 'float', 'decimal_precision': 'Product Unit of Measure'}"/>
<span t-esc="aggregated_lines[line]['product_uom'].name"/>
<span t-if="aggregated_lines[line]['packaging'].name">
(<span t-out="aggregated_lines[line]['packaging_qty_done']" t-options='{"widget": "integer"}'/> <span t-out="aggregated_lines[line]['packaging'].name"/>)
</span>
</t>
</td>
</tr>
@@ -118,6 +118,14 @@
<span t-if="o.state != 'done'" t-field="ml.reserved_uom_qty">3.00</span>
<span t-else="" t-field="ml.qty_done">3.00</span>
<span t-field="ml.product_uom_id" groups="uom.group_uom">units</span>
<span t-if="ml.move_id.product_packaging_id">
<span t-if="o.state != 'done'">
(<span t-field="ml.product_packaging_qty" t-options='{"widget": "integer"}'/> <span t-field="ml.move_id.product_packaging_id.name"/>)
</span>
<span t-if="o.state == 'done'">
(<span t-field="ml.product_packaging_qty_done" t-options='{"widget": "integer"}'/> <span t-field="ml.move_id.product_packaging_id.name"/>)
</span>
</span>
</td>
<td t-if="o.picking_type_id.code != 'incoming'" groups="stock.group_stock_multi_locations">
<span t-esc="ml.location_id.display_name">WH/Stock</span>
+93 -20
View File
@@ -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.