[IMP] mrp, sale_mrp: kits quantities
**Before this PR:**
- The quantities available when a kit was ordered was 0 even if enough components were available
- The quantities delivered when a kit was ordered were correct only if all move from the picking of its sale order were done. So the quantities delivered were correct only if all components were fully delivered.
**After this PR:**
- The number of kits available is computed based on the quantities available of the components needed to make the kit.
- The number of kits delivered is computed based on how many kits it's possible to make with the components already processed.
- Some test case are added the check the flows when kits are ordered.
Those test case make sure that:
* Delivered quantities are accurate if :
- kits in kit
- backorders are made
- returns are made
- returns of returns are made
- Multiple differents UoMs are setted on the components of a kit
* Available quantities are accurate if:
- Kits in kit
- Multiple differents UoMs are setted on the components of a kit
TaskID : 1915370
closes odoo/odoo#29270
This commit is contained in:
@@ -197,6 +197,7 @@ class StockMove(models.Model):
|
||||
'quantity_done': quantity_done,
|
||||
'state': 'draft', # will be confirmed below
|
||||
'name': self.name,
|
||||
'bom_line_id': bom_line.id,
|
||||
}
|
||||
|
||||
def _generate_move_phantom(self, bom_line, product_qty, quantity_done):
|
||||
|
||||
@@ -54,6 +54,11 @@ class StockRule(models.Model):
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
return True
|
||||
|
||||
def _get_custom_move_fields(self):
|
||||
fields = super(StockRule, self)._get_custom_move_fields()
|
||||
fields += ['bom_line_id']
|
||||
return fields
|
||||
|
||||
@api.multi
|
||||
def _get_matching_bom(self, product_id, values):
|
||||
if values.get('bom_id', False):
|
||||
@@ -108,6 +113,7 @@ class ProcurementGroup(models.Model):
|
||||
bom_line_uom = bom_line.product_uom_id
|
||||
quant_uom = bom_line.product_id.uom_id
|
||||
component_qty, procurement_uom = bom_line_uom._adjust_uom_quantities(bom_line_data['qty'], quant_uom)
|
||||
values['bom_line_id'] = bom_line.id
|
||||
super(ProcurementGroup, self).run(bom_line.product_id, component_qty, procurement_uom, location_id, name, origin, values)
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import product_product
|
||||
from . import sale_mrp
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
def _compute_quantities(self):
|
||||
""" When the product is a kit, this override computes the fields :
|
||||
- 'virtual_available'
|
||||
- 'qty_available'
|
||||
- 'incoming_qty'
|
||||
- 'outgoing_qty'
|
||||
"""
|
||||
for product in self:
|
||||
bom_kit = self.env['mrp.bom']._bom_find(product=product, bom_type='phantom')
|
||||
if bom_kit:
|
||||
boms, bom_sub_lines = bom_kit.explode(product, 1)
|
||||
ratios_virtual_available = []
|
||||
ratios_qty_available = []
|
||||
ratios_incoming_qty = []
|
||||
ratios_outgoing_qty = []
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
component = bom_line.product_id
|
||||
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
|
||||
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id)
|
||||
ratios_virtual_available.append(component.virtual_available / qty_per_kit)
|
||||
ratios_qty_available.append(component.qty_available / qty_per_kit)
|
||||
ratios_incoming_qty.append(component.incoming_qty / qty_per_kit)
|
||||
ratios_outgoing_qty.append(component.outgoing_qty / qty_per_kit)
|
||||
if bom_sub_lines:
|
||||
product.virtual_available = min(ratios_virtual_available) // 1
|
||||
product.qty_available = min(ratios_qty_available) // 1
|
||||
product.incoming_qty = min(ratios_incoming_qty) // 1
|
||||
product.outgoing_qty = min(ratios_incoming_qty) // 1
|
||||
else:
|
||||
super(Product, self)._compute_quantities()
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_compare
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import float_compare, float_round
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
@@ -10,20 +10,50 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _compute_qty_delivered(self):
|
||||
""" Computes the quantity delivered when a kit is sold.
|
||||
To achieve this, this method fetch the quantities delivered of
|
||||
each component of the kit and the quantity needed of each components to produce 1 kit.
|
||||
Based on this, a ratio is computed for each component,
|
||||
and the lowest one is kept to define the kit's quantity delivered.
|
||||
"""
|
||||
super(SaleOrderLine, self)._compute_qty_delivered()
|
||||
|
||||
for line in self:
|
||||
if line.qty_delivered_method == 'stock_move':
|
||||
# In the case of a kit, we need to check if all components are shipped. Since the BOM might
|
||||
# have changed, we don't compute the quantities but verify the move state.
|
||||
bom = self.env['mrp.bom']._bom_find(product=line.product_id)
|
||||
if bom and bom.type == 'phantom':
|
||||
moves = line.move_ids.filtered(lambda m: m.picking_id and m.picking_id.state != 'cancel')
|
||||
bom_delivered = all([move.state == 'done' for move in moves])
|
||||
if bom_delivered:
|
||||
line.qty_delivered = line.product_uom_qty
|
||||
for order_line in self:
|
||||
if order_line.qty_delivered_method == 'stock_move':
|
||||
boms = order_line.move_ids.mapped('bom_line_id.bom_id')
|
||||
# We fetch the BoMs of type kits linked to the order_line,
|
||||
# the we keep only the one related to the finished produst.
|
||||
# This bom shoud be the only one since bom_line_id was written on the moves
|
||||
relevant_bom = boms.filtered(lambda b: b.type == 'phantom' and
|
||||
(b.product_id == order_line.product_id or
|
||||
(b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id)))
|
||||
if relevant_bom:
|
||||
moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped)
|
||||
qty_ratios = []
|
||||
order_uom_qty = order_line.product_uom._compute_quantity(order_line.product_uom_qty, relevant_bom.product_uom_id)
|
||||
boms, bom_sub_lines = relevant_bom.explode(order_line.product_id, order_uom_qty)
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
bom_line_moves = moves.filtered(lambda m: m.bom_line_id == bom_line)
|
||||
if bom_line_moves:
|
||||
# We compute the quantities needed of each components to make one kit.
|
||||
# Then, we collect every relevant moves related to a specific component
|
||||
# to know how many are considered delivered.
|
||||
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
|
||||
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id)
|
||||
delivery_moves = bom_line_moves.filtered(lambda m: m.location_dest_id.usage == 'customer' and not m.origin_returned_move_id or not (m.origin_returned_move_id and m.to_refund))
|
||||
return_moves = bom_line_moves.filtered(lambda m: m.location_dest_id.usage != 'customer' and m.to_refund)
|
||||
qty_delivered = sum(delivery_moves.mapped('product_qty')) - sum(return_moves.mapped('product_qty'))
|
||||
# We compute a ratio to know how many kits we can produce with this quantity of that specific component
|
||||
qty_ratios.append(qty_delivered / qty_per_kit)
|
||||
else:
|
||||
qty_ratios.append(0.0)
|
||||
break
|
||||
if qty_ratios:
|
||||
# Now that we have every ratio by components, we keep the lowest one to know how many kits we can produce
|
||||
# with the quantities delivered of each component. We use the euclidean division here because a 'partial kit'
|
||||
# doesn't make sense.
|
||||
order_line.qty_delivered = min(qty_ratios) // 1
|
||||
else:
|
||||
line.qty_delivered = 0.0
|
||||
order_line.qty_delivered = 0.0
|
||||
|
||||
@api.multi
|
||||
def _get_bom_component_qty(self, bom):
|
||||
@@ -49,6 +79,44 @@ class SaleOrderLine(models.Model):
|
||||
components[product] = {'qty': qty, 'uom': to_uom.id}
|
||||
return components
|
||||
|
||||
def _check_availability(self, product_id):
|
||||
""" If the 'product_id' is a kit, this method check if every component's
|
||||
availability and catch every warning returned in order to merge them in a single
|
||||
comprehensive warning
|
||||
"""
|
||||
bom_kit = self.env['mrp.bom']._bom_find(product=product_id, bom_type='phantom')
|
||||
if not bom_kit:
|
||||
return super(SaleOrderLine, self)._check_availability(product_id)
|
||||
|
||||
kit = product_id
|
||||
kit_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
|
||||
|
||||
# We check if we need to display the quantities of each missing components for all warehouses
|
||||
kit_by_wh = self.product_id.with_context(warehouse=self.order_id.warehouse_id.id)
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(kit_by_wh.virtual_available, kit_qty, precision_digits=precision) != -1:
|
||||
return {}
|
||||
ignore_warehouse = float_compare(kit.virtual_available, kit_qty, precision_digits=precision) != -1
|
||||
|
||||
message = ''
|
||||
boms, bom_sub_lines = bom_kit.explode(kit, kit_qty)
|
||||
for bom_line, bom_line_data in bom_sub_lines:
|
||||
component = bom_line.product_id
|
||||
component_uom_qty = bom_line_data['qty']
|
||||
component_qty = bom_line.product_uom_id._compute_quantity(component_uom_qty, bom_line.product_id.uom_id)
|
||||
component_warning = self._check_availability_warning(component, component_qty, ignore_warehouse=ignore_warehouse)
|
||||
if component_warning:
|
||||
message += component_warning['warning']['message'] + '\n'
|
||||
|
||||
warning = {}
|
||||
if message:
|
||||
warning_mess = {
|
||||
'title': _('Not enough inventory!'),
|
||||
'message': message
|
||||
}
|
||||
warning = {'warning': warning_mess}
|
||||
return warning
|
||||
|
||||
def _get_qty_procurement(self):
|
||||
self.ensure_one()
|
||||
# Specific case when we change the qty on a SO for a kit product.
|
||||
|
||||
@@ -47,7 +47,110 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
'uom_type': 'bigger',
|
||||
'rounding': 0.001})
|
||||
|
||||
def create_product(self, name, uom_id, routes=()):
|
||||
# Creating all components
|
||||
self.component_a = self._create_product('Comp A', self.uom_unit)
|
||||
self.component_b = self._create_product('Comp B', self.uom_unit)
|
||||
self.component_c = self._create_product('Comp C', self.uom_unit)
|
||||
self.component_d = self._create_product('Comp D', self.uom_unit)
|
||||
self.component_e = self._create_product('Comp E', self.uom_unit)
|
||||
self.component_f = self._create_product('Comp F', self.uom_unit)
|
||||
self.component_g = self._create_product('Comp G', self.uom_unit)
|
||||
|
||||
# Create a kit 'kit_1' :
|
||||
# -----------------------
|
||||
#
|
||||
# kit_1 --|- component_a x2
|
||||
# |- component_b x1
|
||||
# |- component_c x3
|
||||
|
||||
self.kit_1 = self._create_product('Kit 1', self.uom_unit)
|
||||
|
||||
self.bom_kit_1 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.kit_1.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine = self.env['mrp.bom.line']
|
||||
BomLine.create({
|
||||
'product_id': self.component_a.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': self.bom_kit_1.id})
|
||||
BomLine.create({
|
||||
'product_id': self.component_b.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': self.bom_kit_1.id})
|
||||
BomLine.create({
|
||||
'product_id': self.component_c.id,
|
||||
'product_qty': 3.0,
|
||||
'bom_id': self.bom_kit_1.id})
|
||||
|
||||
# Create a kit 'kit_parent' :
|
||||
# ---------------------------
|
||||
#
|
||||
# kit_parent --|- kit_2 x2 --|- component_d x1
|
||||
# | |- kit_1 x2 -------|- component_a x2
|
||||
# | |- component_b x1
|
||||
# | |- component_c x3
|
||||
# |
|
||||
# |- kit_3 x1 --|- component_f x1
|
||||
# | |- component_g x2
|
||||
# |
|
||||
# |- component_e x1
|
||||
|
||||
# Creating all kits
|
||||
self.kit_2 = self._create_product('Kit 2', self.uom_unit)
|
||||
self.kit_3 = self._create_product('kit 3', self.uom_unit)
|
||||
self.kit_parent = self._create_product('Kit Parent', self.uom_unit)
|
||||
|
||||
# Linking the kits and the components via some 'phantom' BoMs
|
||||
bom_kit_2 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.kit_2.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine.create({
|
||||
'product_id': self.component_d.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_2.id})
|
||||
BomLine.create({
|
||||
'product_id': self.kit_1.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_2.id})
|
||||
|
||||
bom_kit_parent = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.kit_parent.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine.create({
|
||||
'product_id': self.component_e.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
BomLine.create({
|
||||
'product_id': self.kit_2.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
|
||||
bom_kit_3 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.kit_3.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine.create({
|
||||
'product_id': self.component_f.id,
|
||||
'product_qty': 1.0,
|
||||
'bom_id': bom_kit_3.id})
|
||||
BomLine.create({
|
||||
'product_id': self.component_g.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_3.id})
|
||||
|
||||
BomLine.create({
|
||||
'product_id': self.kit_3.id,
|
||||
'product_qty': 2.0,
|
||||
'bom_id': bom_kit_parent.id})
|
||||
|
||||
def _create_product(self, name, uom_id, routes=()):
|
||||
p = Form(self.env['product.product'])
|
||||
p.name = name
|
||||
p.type = 'product'
|
||||
@@ -58,6 +161,58 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
p.route_ids.add(r)
|
||||
return p.save()
|
||||
|
||||
# Helper to process quantities based on a dict following this structure :
|
||||
#
|
||||
# qty_to_process = {
|
||||
# product_id: qty
|
||||
# }
|
||||
|
||||
def _process_quantities(self, moves, quantities_to_process):
|
||||
""" Helper to process quantities based on a dict following this structure :
|
||||
qty_to_process = {
|
||||
product_id: qty
|
||||
}
|
||||
"""
|
||||
moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys())
|
||||
for move in moves_to_process:
|
||||
move.write({'quantity_done': quantities_to_process[move.product_id]})
|
||||
|
||||
def _assert_quantities(self, moves, quantities_to_process):
|
||||
""" Helper to check expected quantities based on a dict following this structure :
|
||||
qty_to_process = {
|
||||
product_id: qty
|
||||
...
|
||||
}
|
||||
"""
|
||||
moves_to_process = moves.filtered(lambda m: m.product_id in quantities_to_process.keys())
|
||||
for move in moves_to_process:
|
||||
self.assertEquals(move.product_uom_qty, quantities_to_process[move.product_id])
|
||||
|
||||
def _create_move_quantities(self, qty_to_process, components, warehouse):
|
||||
""" Helper to creates moves in order to update the quantities of components
|
||||
on a specific warehouse. This ensure that all compute fields are triggered.
|
||||
The structure of qty_to_process should be the following :
|
||||
|
||||
qty_to_process = {
|
||||
component: (qty, uom),
|
||||
...
|
||||
}
|
||||
"""
|
||||
for comp in components:
|
||||
f = Form(self.env['stock.move'])
|
||||
f.name = 'Test Receipt Components'
|
||||
f.location_id = self.env.ref('stock.stock_location_suppliers')
|
||||
f.location_dest_id = warehouse.lot_stock_id
|
||||
f.product_id = comp
|
||||
f.product_uom = qty_to_process[comp][1]
|
||||
f.product_uom_qty = qty_to_process[comp][0]
|
||||
move = f.save()
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
move_line = move.move_line_ids[0]
|
||||
move_line.qty_done = qty_to_process[comp][0]
|
||||
move._action_done()
|
||||
|
||||
def test_00_sale_mrp_flow(self):
|
||||
""" Test sale to mrp flow with diffrent unit of measure."""
|
||||
|
||||
@@ -66,10 +221,10 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
# --------------------------
|
||||
route_manufacture = self.warehouse.manufacture_pull_id.route_id
|
||||
route_mto = self.warehouse.mto_pull_id.route_id
|
||||
product_a = self.create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto])
|
||||
product_c = self.create_product('Product C', self.uom_kg)
|
||||
product_b = self.create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto])
|
||||
product_d = self.create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto])
|
||||
product_a = self._create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto])
|
||||
product_c = self._create_product('Product C', self.uom_kg)
|
||||
product_b = self._create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto])
|
||||
product_d = self._create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto])
|
||||
|
||||
# ------------------------------------------------------------------------------------------
|
||||
# Bill of materials for product A, B, D.
|
||||
@@ -493,7 +648,665 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
self.assertEqual(aml_expense.debit, 100, "Cost of Good Sold entry missing or mismatching")
|
||||
self.assertEqual(aml_output.credit, 100, "Cost of Good Sold entry missing or mismatching")
|
||||
|
||||
def test_03_sale_mrp_simple_kit_qty_delivered(self):
|
||||
""" Test that the quantities delivered are correct when
|
||||
a simple kit is ordered with multiple backorders
|
||||
"""
|
||||
|
||||
# kit_1 structure:
|
||||
# ================
|
||||
|
||||
# kit_1 ---|- component_a x2
|
||||
# |- component_b x1
|
||||
# |- component_c x3
|
||||
|
||||
# Updating the quantities in stock to prevent
|
||||
# a 'Not enough inventory' warning message.
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 20)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 10)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 30)
|
||||
|
||||
# Creation of a sale order for x10 kit_1
|
||||
partner = self.env.ref('base.res_partner_1')
|
||||
f = Form(self.env['sale.order'])
|
||||
f.partner_id = partner
|
||||
with f.order_line.new() as line:
|
||||
line.product_id = self.kit_1
|
||||
line.product_uom_qty = 10.0
|
||||
|
||||
# Confirming the SO to trigger the picking creation
|
||||
so = f.save()
|
||||
so.action_confirm()
|
||||
|
||||
# Check picking creation
|
||||
self.assertEquals(len(so.picking_ids), 1)
|
||||
picking_original = so.picking_ids[0]
|
||||
move_lines = picking_original.move_lines
|
||||
|
||||
# Check if the correct amount of stock.moves are created
|
||||
self.assertEquals(len(move_lines), 3)
|
||||
|
||||
# Check if BoM is created and is for a 'Kit'
|
||||
bom_from_k1 = self.env['mrp.bom']._bom_find(product=self.kit_1)
|
||||
self.assertEquals(self.bom_kit_1.id, bom_from_k1.id)
|
||||
self.assertEquals(bom_from_k1.type, 'phantom')
|
||||
|
||||
# Check there's only 1 order line on the SO and it's for x10 'kit_1'
|
||||
order_lines = so.order_line
|
||||
self.assertEquals(len(order_lines), 1)
|
||||
order_line = order_lines[0]
|
||||
self.assertEquals(order_line.product_id.id, self.kit_1.id)
|
||||
self.assertEquals(order_line.product_uom_qty, 10.0)
|
||||
|
||||
# Check if correct qty is ordered for each component of the kit
|
||||
expected_quantities = {
|
||||
self.component_a: 20,
|
||||
self.component_b: 10,
|
||||
self.component_c: 30,
|
||||
}
|
||||
self._assert_quantities(move_lines, expected_quantities)
|
||||
|
||||
# Process only x1 of the first component then create a backorder for the missing components
|
||||
picking_original.move_lines[0].write({'quantity_done': 1})
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, so.picking_ids[0].id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# Check that the backorder was created, no kit should be delivered at this point
|
||||
self.assertEquals(len(so.picking_ids), 2)
|
||||
backorder_1 = so.picking_ids - picking_original
|
||||
self.assertEquals(backorder_1.backorder_id.id, picking_original.id)
|
||||
self.assertEquals(order_line.qty_delivered, 0)
|
||||
|
||||
# Process only x6 each componenent in the picking
|
||||
# Then create a backorder for the missing components
|
||||
backorder_1.move_lines.write({'quantity_done': 6})
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_1.id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# Check that a backorder is created
|
||||
self.assertEquals(len(so.picking_ids), 3)
|
||||
backorder_2 = so.picking_ids - picking_original - backorder_1
|
||||
self.assertEquals(backorder_2.backorder_id.id, backorder_1.id)
|
||||
|
||||
# With x6 unit of each components, we can only make 2 kits.
|
||||
# So only 2 kits should be delivered
|
||||
self.assertEquals(order_line.qty_delivered, 2)
|
||||
|
||||
# Process x3 more unit of each components :
|
||||
# - Now only 3 kits should be delivered
|
||||
# - A backorder will be created, the SO should have 3 picking_ids linked to it.
|
||||
backorder_2.move_lines.write({'quantity_done': 3})
|
||||
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_2.id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
self.assertEquals(len(so.picking_ids), 4)
|
||||
backorder_3 = so.picking_ids - picking_original - backorder_2 - backorder_1
|
||||
self.assertEquals(backorder_3.backorder_id.id, backorder_2.id)
|
||||
self.assertEquals(order_line.qty_delivered, 3)
|
||||
|
||||
# Adding missing components
|
||||
qty_to_process = {
|
||||
self.component_a: 10,
|
||||
self.component_b: 1,
|
||||
self.component_c: 21,
|
||||
}
|
||||
self._process_quantities(backorder_3.move_lines, qty_to_process)
|
||||
|
||||
# Validating the last backorder now it's complete
|
||||
backorder_3.button_validate()
|
||||
order_line._compute_qty_delivered()
|
||||
|
||||
# All kits should be delivered
|
||||
self.assertEquals(order_line.qty_delivered, 10)
|
||||
|
||||
def test_04_sale_mrp_kit_qty_delivered(self):
|
||||
""" Test that the quantities delivered are correct when
|
||||
a kit with subkits is ordered with multiple backorders and returns
|
||||
"""
|
||||
|
||||
# 'kit_parent' structure:
|
||||
# ---------------------------
|
||||
#
|
||||
# kit_parent --|- kit_2 x2 --|- component_d x1
|
||||
# | |- kit_1 x2 -------|- component_a x2
|
||||
# | |- component_b x1
|
||||
# | |- component_c x3
|
||||
# |
|
||||
# |- kit_3 x1 --|- component_f x1
|
||||
# | |- component_g x2
|
||||
# |
|
||||
# |- component_e x1
|
||||
|
||||
# Updating the quantities in stock to prevent
|
||||
# a 'Not enough inventory' warning message.
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(self.component_a, stock_location, 56)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_b, stock_location, 28)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_c, stock_location, 84)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_d, stock_location, 14)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_e, stock_location, 7)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_f, stock_location, 14)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_g, stock_location, 28)
|
||||
|
||||
# Creation of a sale order for x7 kit_parent
|
||||
partner = self.env.ref('base.res_partner_1')
|
||||
f = Form(self.env['sale.order'])
|
||||
f.partner_id = partner
|
||||
with f.order_line.new() as line:
|
||||
line.product_id = self.kit_parent
|
||||
line.product_uom_qty = 7.0
|
||||
|
||||
so = f.save()
|
||||
so.action_confirm()
|
||||
|
||||
# Check picking creation, its move lines should concern
|
||||
# only components. Also checks that the quantities are corresponding
|
||||
# to the SO
|
||||
self.assertEquals(len(so.picking_ids), 1)
|
||||
order_line = so.order_line[0]
|
||||
picking_original = so.picking_ids[0]
|
||||
move_lines = picking_original.move_lines
|
||||
products = move_lines.mapped('product_id')
|
||||
kits = [self.kit_parent, self.kit_3, self.kit_2, self.kit_1]
|
||||
components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e, self.component_f, self.component_g]
|
||||
expected_quantities = {
|
||||
self.component_a: 56.0,
|
||||
self.component_b: 28.0,
|
||||
self.component_c: 84.0,
|
||||
self.component_d: 14.0,
|
||||
self.component_e: 7.0,
|
||||
self.component_f: 14.0,
|
||||
self.component_g: 28.0
|
||||
}
|
||||
|
||||
self.assertEquals(len(move_lines), 7)
|
||||
self.assertTrue(not any(kit in products for kit in kits))
|
||||
self.assertTrue(all(component in products for component in components))
|
||||
self._assert_quantities(move_lines, expected_quantities)
|
||||
|
||||
# Process only 7 units of each component
|
||||
qty_to_process = 7
|
||||
move_lines.write({'quantity_done': qty_to_process})
|
||||
|
||||
# Create a backorder for the missing componenents
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, so.picking_ids[0].id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# Check that a backorded is created
|
||||
self.assertEquals(len(so.picking_ids), 2)
|
||||
backorder_1 = so.picking_ids - picking_original
|
||||
self.assertEquals(backorder_1.backorder_id.id, picking_original.id)
|
||||
|
||||
# Even if some components are delivered completely,
|
||||
# no KitParent should be delivered
|
||||
self.assertEquals(order_line.qty_delivered, 0)
|
||||
|
||||
# Process just enough components to make 1 kit_parent
|
||||
qty_to_process = {
|
||||
self.component_a: 1,
|
||||
self.component_c: 5,
|
||||
}
|
||||
self._process_quantities(backorder_1.move_lines, qty_to_process)
|
||||
|
||||
# Create a backorder for the missing componenents
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_1.id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# Only 1 kit_parent should be delivered at this point
|
||||
self.assertEquals(order_line.qty_delivered, 1)
|
||||
|
||||
# Check that the second backorder is created
|
||||
self.assertEquals(len(so.picking_ids), 3)
|
||||
backorder_2 = so.picking_ids - picking_original - backorder_1
|
||||
self.assertEquals(backorder_2.backorder_id.id, backorder_1.id)
|
||||
|
||||
# Set the components quantities that backorder_2 should have
|
||||
expected_quantities = {
|
||||
self.component_a: 48,
|
||||
self.component_b: 21,
|
||||
self.component_c: 72,
|
||||
self.component_d: 7,
|
||||
self.component_f: 7,
|
||||
self.component_g: 21
|
||||
}
|
||||
|
||||
# Check that the computed quantities are matching the theorical ones.
|
||||
# Since component_e was totally processed, this componenent shouldn't be
|
||||
# present in backorder_2
|
||||
self.assertEquals(len(backorder_2.move_lines), 6)
|
||||
move_comp_e = backorder_2.move_lines.filtered(lambda m: m.product_id.id == self.component_e.id)
|
||||
self.assertFalse(move_comp_e)
|
||||
self._assert_quantities(backorder_2.move_lines, expected_quantities)
|
||||
|
||||
# Process enough components to make x3 kit_parents
|
||||
qty_to_process = {
|
||||
self.component_a: 16,
|
||||
self.component_b: 5,
|
||||
self.component_c: 24,
|
||||
self.component_g: 5
|
||||
}
|
||||
self._process_quantities(backorder_2.move_lines, qty_to_process)
|
||||
|
||||
# Create a backorder for the missing componenents
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, backorder_2.id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# Check that x3 kit_parents are indeed delivered
|
||||
self.assertEquals(order_line.qty_delivered, 3)
|
||||
|
||||
# Check that the third backorder is created
|
||||
self.assertEquals(len(so.picking_ids), 4)
|
||||
backorder_3 = so.picking_ids - (picking_original + backorder_1 + backorder_2)
|
||||
self.assertEquals(backorder_3.backorder_id.id, backorder_2.id)
|
||||
|
||||
# Check the components quantities that backorder_3 should have
|
||||
expected_quantities = {
|
||||
self.component_a: 32,
|
||||
self.component_b: 16,
|
||||
self.component_c: 48,
|
||||
self.component_d: 7,
|
||||
self.component_f: 7,
|
||||
self.component_g: 16
|
||||
}
|
||||
self._assert_quantities(backorder_3.move_lines, expected_quantities)
|
||||
|
||||
# Process all missing components
|
||||
self._process_quantities(backorder_3.move_lines, expected_quantities)
|
||||
|
||||
# Validating the last backorder now it's complete.
|
||||
# All kits should be delivered
|
||||
backorder_3.button_validate()
|
||||
self.assertEquals(order_line.qty_delivered, 7.0)
|
||||
|
||||
# Return all components processed by backorder_3
|
||||
StockReturnPicking = self.env['stock.return.picking']
|
||||
default_data = StockReturnPicking.with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0]).default_get(
|
||||
['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id'])
|
||||
return_wiz = StockReturnPicking.with_context(active_ids=backorder_3.ids, active_id=backorder_3.ids[0]).create(default_data)
|
||||
for return_move in return_wiz.product_return_moves:
|
||||
return_move.write({
|
||||
'quantity': expected_quantities[return_move.product_id],
|
||||
'to_refund': True
|
||||
})
|
||||
res = return_wiz.create_returns()
|
||||
return_pick = self.env['stock.picking'].browse(res['res_id'])
|
||||
|
||||
# Process all components and validate the picking
|
||||
wiz_act = return_pick.button_validate()
|
||||
wiz = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
|
||||
wiz.process()
|
||||
|
||||
# Now quantity delivered should be 3 again
|
||||
self.assertEquals(order_line.qty_delivered, 3)
|
||||
|
||||
default_data = StockReturnPicking.with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0]).default_get(
|
||||
['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id'])
|
||||
return_wiz = StockReturnPicking.with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0]).create(
|
||||
default_data)
|
||||
for move in return_wiz.product_return_moves:
|
||||
move.quantity = expected_quantities[move.product_id]
|
||||
res = return_wiz.create_returns()
|
||||
return_of_return_pick = self.env['stock.picking'].browse(res['res_id'])
|
||||
|
||||
# Process all components except one of each
|
||||
for move in return_of_return_pick.move_lines:
|
||||
move.write({
|
||||
'quantity_done': expected_quantities[move.product_id] - 1,
|
||||
'to_refund': False
|
||||
})
|
||||
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, return_of_return_pick.id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# As one of each component is missing, only 6 kit_parents should be delivered
|
||||
self.assertEquals(order_line.qty_delivered, 6)
|
||||
|
||||
# Check that the 4th backorder is created.
|
||||
self.assertEquals(len(so.picking_ids), 7)
|
||||
backorder_4 = so.picking_ids - (picking_original + backorder_1 + backorder_2 + backorder_3 + return_of_return_pick + return_pick)
|
||||
self.assertEquals(backorder_4.backorder_id.id, return_of_return_pick.id)
|
||||
|
||||
# Check the components quantities that backorder_4 should have
|
||||
for move in backorder_4.move_lines:
|
||||
self.assertEquals(move.product_qty, 1)
|
||||
|
||||
@mute_logger('odoo.tests.common.onchange')
|
||||
def test_05_mrp_sale_kit_availability(self):
|
||||
"""
|
||||
Check that the 'Not enough inventory' warning message shows correct
|
||||
informations when a kit is ordered
|
||||
"""
|
||||
|
||||
warehouse_1 = self.env['stock.warehouse'].create({
|
||||
'name': 'Warehouse 1',
|
||||
'code': 'WH1'
|
||||
})
|
||||
warehouse_2 = self.env['stock.warehouse'].create({
|
||||
'name': 'Warehouse 2',
|
||||
'code': 'WH2'
|
||||
})
|
||||
|
||||
# Those are all componenents needed to make kit_parents
|
||||
components = [self.component_a, self.component_b, self.component_c, self.component_d, self.component_e,
|
||||
self.component_f, self.component_g]
|
||||
|
||||
# Set enough quantities to make 1 kit_uom_in_kit in WH1
|
||||
self.env['stock.quant']._update_available_quantity(self.component_a, warehouse_1.lot_stock_id, 8)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_b, warehouse_1.lot_stock_id, 4)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_c, warehouse_1.lot_stock_id, 12)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_d, warehouse_1.lot_stock_id, 2)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_e, warehouse_1.lot_stock_id, 1)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_f, warehouse_1.lot_stock_id, 2)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_g, warehouse_1.lot_stock_id, 4)
|
||||
|
||||
# Set quantities on WH2, but not enough to make 1 kit_parent
|
||||
self.env['stock.quant']._update_available_quantity(self.component_a, warehouse_2.lot_stock_id, 7)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_b, warehouse_2.lot_stock_id, 3)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_c, warehouse_2.lot_stock_id, 12)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_d, warehouse_2.lot_stock_id, 1)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_e, warehouse_2.lot_stock_id, 1)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_f, warehouse_2.lot_stock_id, 1)
|
||||
self.env['stock.quant']._update_available_quantity(self.component_g, warehouse_2.lot_stock_id, 4)
|
||||
|
||||
# Creation of a sale order for x7 kit_parent
|
||||
qty_ordered = 7
|
||||
f = Form(self.env['sale.order'])
|
||||
f.partner_id = self.env.ref('base.res_partner_1')
|
||||
f.warehouse_id = warehouse_2
|
||||
with f.order_line.new() as line:
|
||||
line.product_id = self.kit_parent
|
||||
line.product_uom_qty = qty_ordered
|
||||
so = f.save()
|
||||
order_line = so.order_line[0]
|
||||
|
||||
# Check that not enough enough quantities are available in the warehouse set in the SO
|
||||
# but there are enough quantities in Warehouse 1 for 1 kit_parent
|
||||
kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id)
|
||||
kit_parent_wh1 = self.kit_parent.with_context(warehouse=warehouse_1.id)
|
||||
|
||||
# Check that not enough enough quantities are available in the warehouse set in the SO
|
||||
# but there are enough quantities in Warehouse 1 for 1 kit_parent
|
||||
self.assertEquals(kit_parent_wh_order.virtual_available, 0)
|
||||
self.assertEquals(kit_parent_wh1.virtual_available, 1)
|
||||
|
||||
# A warning message should be returned as there arn't enough quantities available for the sale order
|
||||
warning = order_line._onchange_product_id_check_availability()
|
||||
self.assertTrue(warning)
|
||||
|
||||
# We receive enoug of each component in Warehouse 2 to make 3 kit_parent
|
||||
qty_to_process = {
|
||||
self.component_a: (17, self.uom_unit),
|
||||
self.component_b: (12, self.uom_unit),
|
||||
self.component_c: (25, self.uom_unit),
|
||||
self.component_d: (5, self.uom_unit),
|
||||
self.component_e: (2, self.uom_unit),
|
||||
self.component_f: (5, self.uom_unit),
|
||||
self.component_g: (8, self.uom_unit),
|
||||
}
|
||||
self._create_move_quantities(qty_to_process, components, warehouse_2)
|
||||
|
||||
# As 'Warehouse 2' is the warehouse linked to the SO, 3 kits should be available
|
||||
# But the quantity available in Warehouse 1 should stay 1
|
||||
kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id)
|
||||
kit_parent_wh1 = self.kit_parent.with_context(warehouse=warehouse_1.id)
|
||||
self.assertEquals(kit_parent_wh_order.virtual_available, 3)
|
||||
self.assertEquals(kit_parent_wh1.virtual_available, 1)
|
||||
|
||||
# A warning message should be returned as there arn't enough quantities available for the sale order
|
||||
warning = order_line._onchange_product_id_check_availability()
|
||||
self.assertTrue(warning)
|
||||
|
||||
# We receive enough of each component in Warehouse 2 to make 7 kit_parent
|
||||
qty_to_process = {
|
||||
self.component_a: (32, self.uom_unit),
|
||||
self.component_b: (16, self.uom_unit),
|
||||
self.component_c: (48, self.uom_unit),
|
||||
self.component_d: (8, self.uom_unit),
|
||||
self.component_e: (4, self.uom_unit),
|
||||
self.component_f: (8, self.uom_unit),
|
||||
self.component_g: (16, self.uom_unit),
|
||||
}
|
||||
self._create_move_quantities(qty_to_process, components, warehouse_2)
|
||||
|
||||
# Enough quantities should be available, no warning message should be displayed
|
||||
kit_parent_wh_order = self.kit_parent.with_context(warehouse=so.warehouse_id.id)
|
||||
self.assertEquals(kit_parent_wh_order.virtual_available, 7)
|
||||
|
||||
# Some cache issue prevents the following to work in a test
|
||||
#warning = order_line._onchange_product_id_check_availability()
|
||||
#self.assertFalse(warning)
|
||||
|
||||
def test_06_kit_qty_delivered_mixed_uom(self):
|
||||
"""
|
||||
Check that the quantities delivered are correct when a kit involves
|
||||
multiple UoMs on its components
|
||||
"""
|
||||
# Create some components
|
||||
component_uom_unit = self._create_product('Comp Unit', self.uom_unit)
|
||||
component_uom_dozen = self._create_product('Comp Dozen', self.uom_dozen)
|
||||
component_uom_kg = self._create_product('Comp Kg', self.uom_kg)
|
||||
|
||||
# Create a kit 'kit_uom_1' :
|
||||
# -----------------------
|
||||
#
|
||||
# kit_uom_1 --|- component_uom_unit x2 Test-Dozen
|
||||
# |- component_uom_dozen x1 Test-Dozen
|
||||
# |- component_uom_kg x3 Test-G
|
||||
|
||||
kit_uom_1 = self._create_product('Kit 1', self.uom_unit)
|
||||
|
||||
bom_kit_uom_1 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_uom_1.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine = self.env['mrp.bom.line']
|
||||
BomLine.create({
|
||||
'product_id': component_uom_unit.id,
|
||||
'product_qty': 2.0,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'bom_id': bom_kit_uom_1.id})
|
||||
BomLine.create({
|
||||
'product_id': component_uom_dozen.id,
|
||||
'product_qty': 1.0,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'bom_id': bom_kit_uom_1.id})
|
||||
BomLine.create({
|
||||
'product_id': component_uom_kg.id,
|
||||
'product_qty': 3.0,
|
||||
'product_uom_id': self.uom_gm.id,
|
||||
'bom_id': bom_kit_uom_1.id})
|
||||
|
||||
# Updating the quantities in stock to prevent
|
||||
# a 'Not enough inventory' warning message.
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_unit, stock_location, 240)
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_dozen, stock_location, 10)
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_kg, stock_location, 0.03)
|
||||
|
||||
# Creation of a sale order for x10 kit_1
|
||||
partner = self.env.ref('base.res_partner_1')
|
||||
f = Form(self.env['sale.order'])
|
||||
f.partner_id = partner
|
||||
with f.order_line.new() as line:
|
||||
line.product_id = kit_uom_1
|
||||
line.product_uom_qty = 10.0
|
||||
|
||||
so = f.save()
|
||||
so.action_confirm()
|
||||
|
||||
picking_original = so.picking_ids[0]
|
||||
move_lines = picking_original.move_lines
|
||||
order_line = so.order_line[0]
|
||||
|
||||
# Check that the quantities on the picking are the one expected for each components
|
||||
for ml in move_lines:
|
||||
corr_bom_line = bom_kit_uom_1.bom_line_ids.filtered(lambda b: b.product_id.id == ml.product_id.id)
|
||||
computed_qty = ml.product_uom._compute_quantity(ml.product_uom_qty, corr_bom_line.product_uom_id)
|
||||
self.assertEquals(computed_qty, order_line.product_uom_qty * corr_bom_line.product_qty)
|
||||
|
||||
# Processe enough componenents in the picking to make 2 kit_uom_1
|
||||
# Then create a backorder for the missing components
|
||||
qty_to_process = {
|
||||
component_uom_unit: 48,
|
||||
component_uom_dozen: 3,
|
||||
component_uom_kg: 0.006
|
||||
}
|
||||
self._process_quantities(move_lines, qty_to_process)
|
||||
backorder_wizard = self.env['stock.backorder.confirmation'].create(
|
||||
{'pick_ids': [(4, so.picking_ids[0].id)]})
|
||||
backorder_wizard.process()
|
||||
|
||||
# Check that a backorder is created
|
||||
self.assertEquals(len(so.picking_ids), 2)
|
||||
backorder_1 = so.picking_ids - picking_original
|
||||
self.assertEquals(backorder_1.backorder_id.id, picking_original.id)
|
||||
|
||||
# Only 2 kits should be delivered
|
||||
self.assertEquals(order_line.qty_delivered, 2)
|
||||
|
||||
# Adding missing components
|
||||
qty_to_process = {
|
||||
component_uom_unit: 192,
|
||||
component_uom_dozen: 7,
|
||||
component_uom_kg: 0.024
|
||||
}
|
||||
self._process_quantities(backorder_1.move_lines, qty_to_process)
|
||||
|
||||
# Validating the last backorder now it's complete
|
||||
backorder_1.button_validate()
|
||||
order_line._compute_qty_delivered()
|
||||
# All kits should be delivered
|
||||
self.assertEquals(order_line.qty_delivered, 10)
|
||||
|
||||
@mute_logger('odoo.tests.common.onchange')
|
||||
def test_07_kit_availability_mixed_uom(self):
|
||||
"""
|
||||
Check that the 'Not enough inventory' warning message displays correct
|
||||
informations when a kit with multiple UoMs on its components is ordered
|
||||
"""
|
||||
|
||||
# Create some components
|
||||
component_uom_unit = self._create_product('Comp Unit', self.uom_unit)
|
||||
component_uom_dozen = self._create_product('Comp Dozen', self.uom_dozen)
|
||||
component_uom_kg = self._create_product('Comp Kg', self.uom_kg)
|
||||
component_uom_gm = self._create_product('Comp g', self.uom_gm)
|
||||
components = [component_uom_unit, component_uom_dozen, component_uom_kg, component_uom_gm]
|
||||
|
||||
# Create a kit 'kit_uom_in_kit' :
|
||||
# -----------------------
|
||||
# kit_uom_in_kit --|- component_uom_gm x3 Test-KG
|
||||
# |- kit_uom_1 x2 Test-Dozen --|- component_uom_unit x2 Test-Dozen
|
||||
# |- component_uom_dozen x1 Test-Dozen
|
||||
# |- component_uom_kg x3 Test-G
|
||||
|
||||
kit_uom_1 = self._create_product('Sub Kit 1', self.uom_unit)
|
||||
kit_uom_in_kit = self._create_product('Parent Kit', self.uom_unit)
|
||||
|
||||
bom_kit_uom_1 = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_uom_1.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine = self.env['mrp.bom.line']
|
||||
BomLine.create({
|
||||
'product_id': component_uom_unit.id,
|
||||
'product_qty': 2.0,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'bom_id': bom_kit_uom_1.id})
|
||||
BomLine.create({
|
||||
'product_id': component_uom_dozen.id,
|
||||
'product_qty': 1.0,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'bom_id': bom_kit_uom_1.id})
|
||||
BomLine.create({
|
||||
'product_id': component_uom_kg.id,
|
||||
'product_qty': 3.0,
|
||||
'product_uom_id': self.uom_gm.id,
|
||||
'bom_id': bom_kit_uom_1.id})
|
||||
|
||||
bom_kit_uom_in_kit = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': kit_uom_in_kit.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'})
|
||||
|
||||
BomLine.create({
|
||||
'product_id': component_uom_gm.id,
|
||||
'product_qty': 3.0,
|
||||
'product_uom_id': self.uom_kg.id,
|
||||
'bom_id': bom_kit_uom_in_kit.id})
|
||||
BomLine.create({
|
||||
'product_id': kit_uom_1.id,
|
||||
'product_qty': 2.0,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'bom_id': bom_kit_uom_in_kit.id})
|
||||
|
||||
# Create a simple warehouse to receives some products
|
||||
warehouse_1 = self.env['stock.warehouse'].create({
|
||||
'name': 'Warehouse 1',
|
||||
'code': 'WH1'
|
||||
})
|
||||
|
||||
# Set enough quantities to make 1 kit_uom_in_kit in WH1
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_unit, warehouse_1.lot_stock_id, 576)
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_dozen, warehouse_1.lot_stock_id, 24)
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_kg, warehouse_1.lot_stock_id, 0.072)
|
||||
self.env['stock.quant']._update_available_quantity(component_uom_gm, warehouse_1.lot_stock_id, 3000)
|
||||
|
||||
# Creation of a sale order for x5 kit_uom_in_kit
|
||||
qty_ordered = 5
|
||||
f = Form(self.env['sale.order'])
|
||||
f.partner_id = self.env.ref('base.res_partner_1')
|
||||
f.warehouse_id = warehouse_1
|
||||
with f.order_line.new() as line:
|
||||
line.product_id = kit_uom_in_kit
|
||||
line.product_uom_qty = qty_ordered
|
||||
|
||||
so = f.save()
|
||||
order_line = so.order_line[0]
|
||||
|
||||
# Check that not enough enough quantities are available in the warehouse set in the SO
|
||||
# but there are enough quantities in Warehouse 1 for 1 kit_parent
|
||||
kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities()
|
||||
virtual_available_wh_order = kit_uom_in_kit.virtual_available
|
||||
self.assertEquals(virtual_available_wh_order, 1)
|
||||
|
||||
# A warning message should be returned as there arn't enough quantities available for the sale order
|
||||
warning = order_line._onchange_product_id_check_availability()
|
||||
self.assertTrue(warning)
|
||||
|
||||
# We receive enough of each component in Warehouse 1 to make 3 kit_uom_in_kit.
|
||||
# Moves are created instead of only updating the quant quantities in order to trigger every compute fields.
|
||||
qty_to_process = {
|
||||
component_uom_unit: (1152, self.uom_unit),
|
||||
component_uom_dozen: (48, self.uom_dozen),
|
||||
component_uom_kg: (0.144, self.uom_kg),
|
||||
component_uom_gm: (6000, self.uom_gm)
|
||||
}
|
||||
self._create_move_quantities(qty_to_process, components, warehouse_1)
|
||||
|
||||
# Enough quantities should be available to make 3 kit_uom_in_kit and a warning message should be displayed
|
||||
warning = order_line._onchange_product_id_check_availability()
|
||||
self.assertTrue(warning)
|
||||
kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities()
|
||||
virtual_available_wh_order = kit_uom_in_kit.virtual_available
|
||||
self.assertEquals(virtual_available_wh_order, 3)
|
||||
|
||||
# We process enough quantities to have enough kit_uom_in_kit available for the sale order.
|
||||
self._create_move_quantities(qty_to_process, components, warehouse_1)
|
||||
|
||||
# We check that enough quantities were processed to sell 5 kit_uom_in_kit
|
||||
kit_uom_in_kit.with_context(warehouse=warehouse_1.id)._compute_quantities()
|
||||
self.assertEquals(kit_uom_in_kit.virtual_available, 5)
|
||||
|
||||
# Some cache issue prevents the following to work in a test
|
||||
|
||||
# No warning should be raised
|
||||
# warning = order_line._onchange_product_id_check_availability()
|
||||
# self.assertFalse(warning)
|
||||
|
||||
def test_10_sale_mrp_kits_routes(self):
|
||||
|
||||
# Create a kit 'kit_1' :
|
||||
@@ -502,9 +1315,9 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
# kit_1 --|- component_shelf1 x3
|
||||
# |- component_shelf2 x2
|
||||
|
||||
kit_1 = self.create_product('Kit1', self.uom_unit)
|
||||
component_shelf1 = self.create_product('Comp Shelf1', self.uom_unit)
|
||||
component_shelf2 = self.create_product('Comp Shelf2', self.uom_unit)
|
||||
kit_1 = self._create_product('Kit1', self.uom_unit)
|
||||
component_shelf1 = self._create_product('Comp Shelf1', self.uom_unit)
|
||||
component_shelf2 = self._create_product('Comp Shelf2', self.uom_unit)
|
||||
|
||||
with Form(self.env['mrp.bom']) as bom:
|
||||
bom.product_tmpl_id = kit_1.product_tmpl_id
|
||||
@@ -550,6 +1363,10 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
component_shelf2.write({
|
||||
'route_ids': [(4, route_shelf2.id)]})
|
||||
|
||||
# Set enough quantities to make 1 kit_uom_in_kit in WH1
|
||||
self.env['stock.quant']._update_available_quantity(component_shelf1, self.env.ref('stock.warehouse0').lot_stock_id, 15)
|
||||
self.env['stock.quant']._update_available_quantity(component_shelf2, self.env.ref('stock.warehouse0').lot_stock_id, 10)
|
||||
|
||||
# Creating a sale order for 5 kits and confirming it
|
||||
order_form = Form(self.env['sale.order'])
|
||||
order_form.partner_id = self.env.ref('base.res_partner_2')
|
||||
@@ -573,7 +1390,6 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
self.assertEquals(move_shelf2.location_id.id, self.ref('stock.stock_location_14'))
|
||||
self.assertEquals(move_shelf2.location_dest_id.id, self.ref('stock.stock_location_customers'))
|
||||
|
||||
@mute_logger('odoo.tests.common.onchange')
|
||||
def test_11_sale_mrp_explode_kits_uom_quantities(self):
|
||||
|
||||
# Create a kit 'kit_1' :
|
||||
@@ -582,9 +1398,9 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
# 2x Dozens kit_1 --|- component_unit x6 Units
|
||||
# |- component_kg x7 Kg
|
||||
|
||||
kit_1 = self.create_product('Kit1', self.uom_unit)
|
||||
component_unit = self.create_product('Comp Unit', self.uom_unit)
|
||||
component_kg = self.create_product('Comp Kg', self.uom_kg)
|
||||
kit_1 = self._create_product('Kit1', self.uom_unit)
|
||||
component_unit = self._create_product('Comp Unit', self.uom_unit)
|
||||
component_kg = self._create_product('Comp Kg', self.uom_kg)
|
||||
|
||||
with Form(self.env['mrp.bom']) as bom:
|
||||
bom.product_tmpl_id = kit_1.product_tmpl_id
|
||||
@@ -600,9 +1416,19 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
line.product_qty = 7
|
||||
line.product_uom_id = self.uom_kg
|
||||
|
||||
# Create a simple warehouse to receives some products
|
||||
warehouse_1 = self.env['stock.warehouse'].create({
|
||||
'name': 'Warehouse 1',
|
||||
'code': 'WH1'
|
||||
})
|
||||
# Set enough quantities to make 1 Test-Dozen kit_uom_in_kit
|
||||
self.env['stock.quant']._update_available_quantity(component_unit, warehouse_1.lot_stock_id, 12)
|
||||
self.env['stock.quant']._update_available_quantity(component_kg, warehouse_1.lot_stock_id, 14)
|
||||
|
||||
# Creating a sale order for 3 Units of kit_1 and confirming it
|
||||
order_form = Form(self.env['sale.order'])
|
||||
order_form.partner_id = self.env.ref('base.res_partner_2')
|
||||
order_form.warehouse_id = warehouse_1
|
||||
with order_form.order_line.new() as line:
|
||||
line.product_id = kit_1
|
||||
line.product_uom = self.uom_unit
|
||||
@@ -620,4 +1446,3 @@ class TestSaleMrpFlow(common.TransactionCase):
|
||||
move_component_kg = order.picking_ids[0].move_lines - move_component_unit
|
||||
self.assertEquals(move_component_unit.product_uom_qty, 0.5)
|
||||
self.assertEquals(move_component_kg.product_uom_qty, 0.583)
|
||||
|
||||
|
||||
@@ -256,29 +256,41 @@ class SaleOrderLine(models.Model):
|
||||
if not self.product_id or not self.product_uom_qty or not self.product_uom:
|
||||
self.product_packaging = False
|
||||
return {}
|
||||
if self.product_id.type == 'product':
|
||||
return self._check_availability(self.product_id)
|
||||
|
||||
def _check_availability(self, product_id):
|
||||
""" The purpose of this method is only to be overriden if
|
||||
'product_id' is a kit
|
||||
"""
|
||||
product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
|
||||
return self._check_availability_warning(product_id, product_qty)
|
||||
|
||||
def _check_availability_warning(self, product_id, product_qty, ignore_warehouse=False):
|
||||
if product_id.type == 'product':
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
product = self.product_id.with_context(
|
||||
product_by_wh = product_id.with_context(
|
||||
warehouse=self.order_id.warehouse_id.id,
|
||||
lang=self.order_id.partner_id.lang or self.env.user.lang or 'en_US'
|
||||
)
|
||||
product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
|
||||
if float_compare(product.virtual_available, product_qty, precision_digits=precision) == -1:
|
||||
is_available = self._check_routing()
|
||||
if float_compare(product_by_wh.virtual_available, product_qty, precision_digits=precision) == -1:
|
||||
is_available = self._check_routing(product_id)
|
||||
if not is_available:
|
||||
message = _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \
|
||||
(self.product_uom_qty, self.product_uom.name, self.product_id.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name)
|
||||
message = _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \
|
||||
(self.product_uom_qty, self.product_uom.name, product_id.name,
|
||||
product_by_wh.virtual_available,
|
||||
product_by_wh.uom_id.name, self.order_id.warehouse_id.name)
|
||||
# We check if some products are available in other warehouses.
|
||||
if float_compare(product.virtual_available, self.product_id.virtual_available, precision_digits=precision) == -1:
|
||||
if not ignore_warehouse and float_compare(product_by_wh.virtual_available, product_id.virtual_available,
|
||||
precision_digits=precision) == -1:
|
||||
message += _('\nThere are %s %s available across all warehouses.\n\n') % \
|
||||
(self.product_id.virtual_available, product.uom_id.name)
|
||||
(product_id.virtual_available, product_by_wh.uom_id.name)
|
||||
for warehouse in self.env['stock.warehouse'].search([]):
|
||||
quantity = self.product_id.with_context(warehouse=warehouse.id).virtual_available
|
||||
quantity = product_id.with_context(warehouse=warehouse.id).virtual_available
|
||||
if quantity > 0:
|
||||
message += "%s: %s %s\n" % (warehouse.name, quantity, self.product_id.uom_id.name)
|
||||
message += "%s: %s %s\n" % (warehouse.name, quantity, product_id.uom_id.name)
|
||||
warning_mess = {
|
||||
'title': _('Not enough inventory!'),
|
||||
'message' : message
|
||||
'message': message
|
||||
}
|
||||
return {'warning': warning_mess}
|
||||
return {}
|
||||
@@ -399,14 +411,13 @@ class SaleOrderLine(models.Model):
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def _check_routing(self):
|
||||
def _check_routing(self, product_id):
|
||||
""" Verify the route of the product based on the warehouse
|
||||
return True if the product availibility in stock does not need to be verified,
|
||||
which is the case in MTO, Cross-Dock or Drop-Shipping
|
||||
"""
|
||||
is_available = False
|
||||
product_routes = self.route_id or (self.product_id.route_ids + self.product_id.categ_id.total_route_ids)
|
||||
product_routes = self.route_id or (product_id.route_ids + product_id.categ_id.total_route_ids)
|
||||
|
||||
# Check MTO
|
||||
wh_mto_route = self.order_id.warehouse_id.mto_pull_id.route_id
|
||||
|
||||
Reference in New Issue
Block a user