[IMP] stock: backorder confirmation

In `_check_backorder`, changes the condition so it checks if the qty
done is enough compared to the actual reserved quantity (instead of the
demand).
Also, removes an unused bloack of code and makes minor visual changes.

task-3076044

Part-of: odoo/odoo#109511
This commit is contained in:
svs-odoo
2023-02-13 15:15:08 +01:00
parent 4b6a0e6b5b
commit 656d8ace87
12 changed files with 97 additions and 99 deletions
+1 -3
View File
@@ -57,9 +57,7 @@ class TestPoSSale(TestPointOfSaleHttpCommon):
sale_order.action_confirm()
picking = sale_order.picking_ids
picking.move_ids.quantity_done = 300
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context']))
wizard.save().process()
picking.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 1)
+1 -3
View File
@@ -550,9 +550,7 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
# deliver partially (1 of each instead of 5), check the so's invoice_status and delivered quantities
pick = so.picking_ids
pick.move_ids.write({'quantity_done': 1})
wiz_act = pick.button_validate()
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
wiz.process()
pick.button_validate()
self.assertEqual(so.invoice_status, 'no', 'Sale MRP: so invoice_status should be "no" after partial delivery of a kit')
del_qty = sum(sol.qty_delivered for sol in so.order_line)
self.assertEqual(del_qty, 0.0, 'Sale MRP: delivered quantity should be zero after partial delivery of a kit')
@@ -127,9 +127,7 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
# Deliver one.
sale_order.picking_ids.move_ids.quantity_done = 1
wiz = sale_order.picking_ids.button_validate()
wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
wiz.process()
sale_order.picking_ids.button_validate()
# Invoice 1
invoice = sale_order._create_invoices()
@@ -266,9 +264,7 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
# Deliver one.
sale_order.picking_ids.move_ids.quantity_done = 1
wiz = sale_order.picking_ids.button_validate()
wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
wiz.process()
sale_order.picking_ids.button_validate()
# Invoice 1
invoice = sale_order._create_invoices()
+4 -5
View File
@@ -316,16 +316,15 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon):
# deliver them
# One of the move is for a consumable product, thus is assigned. The second one is for a
# storable product, thus is unavailable. Hitting `button_validate` will first ask to
# process all the reserved quantities and, if the user chose to process, a second wizard
# will ask to create a backorder for the unavailable product.
# process all the reserved quantities and, if the user chose to process, a backorder will
# be created for the unavailable product.
self.assertEqual(len(self.so.picking_ids), 1)
res_dict = self.so.picking_ids.sorted()[0].button_validate()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
self.assertEqual(wizard._name, 'stock.immediate.transfer')
res_dict = wizard.process()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
self.assertEqual(wizard._name, 'stock.backorder.confirmation')
wizard.process()
self.assertEqual(len(self.so.picking_ids), 2)
self.assertEqual(self.so.picking_ids[0].backorder_id.id, self.so.picking_ids[1].id)
# Now, the original picking is done and there is a new one (the backorder).
self.assertEqual(len(self.so.picking_ids), 2)
@@ -321,9 +321,7 @@ class TestSaleStockInvoices(TestSaleCommon):
line.lot_id = lot02
line.qty_done = 3
move_form.save()
action = delivery02.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
delivery02.button_validate()
# Invoice 2 x P
invoice01 = so._create_invoices()
@@ -158,9 +158,7 @@ class TestSaleStockMargin(TestStockValuationCommon):
sale_order.picking_ids.move_ids[0].quantity_done = 2
sale_order.picking_ids.move_ids[1].quantity_done = 3
res = sale_order.picking_ids.button_validate()
Form(self.env[res['res_model']].with_context(res['context'])).save().process()
sale_order.picking_ids.button_validate()
self.assertAlmostEqual(order_line_1.purchase_price, 43) # (35 + 51) / 2
self.assertAlmostEqual(order_line_2.purchase_price, 12.5) # (17 + 11 + 11 + 11) / 4
+4 -10
View File
@@ -1176,19 +1176,13 @@ class Picking(models.Model):
continue
quantity_todo = {}
quantity_done = {}
for move in picking.move_ids.filtered(lambda m: m.state != "cancel"):
for move in picking.move_ids:
if move.state == "cancel":
continue
quantity_todo.setdefault(move.product_id.id, 0)
quantity_done.setdefault(move.product_id.id, 0)
quantity_todo[move.product_id.id] += move.product_uom._compute_quantity(move.product_uom_qty, move.product_id.uom_id, rounding_method='HALF-UP')
quantity_todo[move.product_id.id] += sum(move.move_line_ids.mapped('reserved_qty'))
quantity_done[move.product_id.id] += move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')
# FIXME: the next block doesn't seem nor should be used.
for ops in picking.mapped('move_line_ids').filtered(lambda x: x.package_id and not x.product_id and not x.move_id):
for quant in ops.package_id.quant_ids:
quantity_done.setdefault(quant.product_id.id, 0)
quantity_done[quant.product_id.id] += quant.qty
for pack in picking.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id):
quantity_done.setdefault(pack.product_id.id, 0)
quantity_done[pack.product_id.id] += pack.product_uom_id._compute_quantity(pack.qty_done, pack.product_id.uom_id)
if any(
float_compare(quantity_done[x], quantity_todo.get(x, 0), precision_digits=prec,) == -1
for x in quantity_done
+11 -22
View File
@@ -2719,10 +2719,8 @@ class StockMove(TransactionCase):
# the quantity done on the move should not respect the rounding of the move line
self.assertEqual(move_stock_pack.quantity_done, 0.5)
# create the backorder in the uom of the quants
backorder_wizard_dict = picking_stock_pack.button_validate()
backorder_wizard = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context'])).save()
backorder_wizard.process()
# Validate the picking should create a backorder in the uom of the quants.
picking_stock_pack.button_validate()
self.assertEqual(move_stock_pack.state, 'done')
self.assertEqual(move_stock_pack.quantity_done, 0.5)
self.assertEqual(move_stock_pack.product_uom_qty, 0.5)
@@ -4234,9 +4232,9 @@ class StockMove(TransactionCase):
def test_immediate_validate_2(self):
""" In a picking with a single partially available move, clicking on validate without
filling any quantities should open a wizard asking to process all the reservation (so, only
a part of the initial demand). Validating this wizard should open another one asking for
the creation of a backorder. If the backorder is created, it should contain the quantities
not processed.
a part of the initial demand). Validating this wizard should validate the picking and create
a backorder (the user's confirmation is asked only when the reservation is not fully
processed). The created backorder should contain the quantities not processed.
"""
partner = self.env['res.partner'].create({'name': 'Jean'})
self.env['stock.quant']._update_available_quantity(self.product, self.stock_location, 5.0)
@@ -4261,11 +4259,7 @@ class StockMove(TransactionCase):
res_dict = picking.button_validate()
self.assertEqual(res_dict.get('res_model'), 'stock.immediate.transfer')
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
res_dict_for_back_order = wizard.process()
self.assertEqual(res_dict_for_back_order.get('res_model'), 'stock.backorder.confirmation')
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
# Chose to create a backorder.
backorder_wizard.process()
wizard.process()
# Only 5 products should be processed on the initial move.
self.assertEqual(picking.move_ids.state, 'done')
@@ -4281,9 +4275,8 @@ class StockMove(TransactionCase):
def test_immediate_validate_3(self):
""" In a picking with two moves, one partially available and one unavailable, clicking
on validate without filling any quantities should open a wizard asking to process all the
reservation (so, only a part of one of the moves). Validating this wizard should open
another one asking for the creation of a backorder. If the backorder is created, it should
contain the quantities not processed.
reservation (so, only a part of one of the moves). Validating this wizard should confirm the
picking and create a backorder with the quantities not processed.
"""
product5 = self.env['product.product'].create({
'name': 'Product 5',
@@ -4327,10 +4320,6 @@ class StockMove(TransactionCase):
action = picking.button_validate()
self.assertEqual(action.get('res_model'), 'stock.immediate.transfer')
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
action = wizard.process()
self.assertTrue(isinstance(action, dict), 'Should open backorder wizard')
self.assertEqual(action.get('res_model'), 'stock.backorder.confirmation')
wizard = self.env[(action.get('res_model'))].browse(action.get('res_id')).with_context(action.get('context'))
wizard.process()
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
self.assertEqual(len(backorder), 1.0)
@@ -5750,10 +5739,10 @@ class StockMove(TransactionCase):
delivery_form = Form(picking)
delivery = delivery_form.save()
delivery.action_confirm()
delivery.button_validate()
backorder_wizard_dict = delivery.button_validate()
backorder_wizard_form = Form(self.env[backorder_wizard_dict['res_model']].with_context(backorder_wizard_dict['context']))
backorder_wizard_form.save().process_cancel_backorder() # Don't create a backorder
backorder = self.env['stock.picking'].search([('backorder_id', '=', delivery.id)])
backorder.action_cancel() # Cancels the automatically created backorder.
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}']
+1 -3
View File
@@ -644,9 +644,7 @@ class TestPickShip(TestStockCommon):
# make some stock
self.env['stock.quant']._update_available_quantity(self.productA, location, 10.0)
picking_pick.move_ids.quantity_done = 5.0
backorder_wizard_values = picking_pick.button_validate()
backorder_wizard = self.env[(backorder_wizard_values.get('res_model'))].browse(backorder_wizard_values.get('res_id')).with_context(backorder_wizard_values['context'])
backorder_wizard.process()
picking_pick.button_validate()
self.assertTrue(picking_client.move_line_ids, 'A move line should be created.')
self.assertEqual(picking_client.move_line_ids.reserved_uom_qty, 5, 'The move line should have 5 unit reserved.')
+6 -16
View File
@@ -656,9 +656,7 @@ class TestStockFlow(TestStockCommon):
res_dict = picking_in_B.button_validate()
wizard = Form(self.env[res_dict.get('res_model')].with_context(res_dict['context'])).save()
res_dict_for_back_order = wizard.process()
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
backorder_wizard.process()
wizard.process()
# -----------------------------------------------------------------------
# Check incoming shipment
@@ -969,9 +967,7 @@ class TestStockFlow(TestStockCommon):
pack_opt.write({'reserved_uom_qty': 5})
res_dict = picking_out.button_validate()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
res_dict_for_back_order = wizard.process()
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
backorder_wizard.process()
wizard.process()
quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
# Check total quantity stock location.
@@ -993,9 +989,7 @@ class TestStockFlow(TestStockCommon):
pack_opt.write({'reserved_uom_qty': 5})
res_dict = bo_out_1.button_validate()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
res_dict_for_back_order = wizard.process()
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
backorder_wizard.process()
wizard.process()
quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
@@ -1017,9 +1011,7 @@ class TestStockFlow(TestStockCommon):
pack_opt.write({'reserved_uom_qty': 5})
res_dict = bo_out_2.button_validate()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
res_dict_for_back_order = wizard.process()
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
backorder_wizard.process()
wizard.process()
# Check total quantity stock location of product KG.
quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
@@ -1040,9 +1032,7 @@ class TestStockFlow(TestStockCommon):
pack_opt.write({'reserved_uom_qty': 5})
res_dict = bo_out_3.button_validate()
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
res_dict_for_back_order = wizard.process()
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id')).with_context(res_dict_for_back_order['context'])
backorder_wizard.process()
wizard.process()
quants = self.StockQuantObj.search([('product_id', '=', productKG.id), ('location_id', '=', self.stock_location)])
total_qty = [quant.quantity for quant in quants]
self.assertEqual(sum(total_qty), 999.980, 'Expecting 999.980 kg , got %.4f kg on location stock!' % (sum(total_qty)))
@@ -2161,7 +2151,7 @@ class TestStockFlow(TestStockCommon):
'location_id': stock_location.id,
'location_dest_id': self.customer_location
})
picking_out.action_confirm()
move_out.quantity_done = 7
action_dict = picking_out.button_validate()
@@ -40,8 +40,8 @@
<footer>
<button name="process" string="Create Backorder" type="object" class="oe_highlight" data-hotkey="q"/>
<button name="process_cancel_backorder" string="No Backorder" type="object" class="btn-primary" attrs="{'invisible': [('show_transfers', '=', True)]}" data-hotkey="w"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
<button name="process_cancel_backorder" string="No Backorder" type="object" class="btn-danger text-uppercase" attrs="{'invisible': [('show_transfers', '=', True)]}" data-hotkey="w"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
@@ -217,9 +217,48 @@ class TestBatchPicking(TransactionCase):
self.assertFalse(sum(quant_B.mapped('quantity')))
def test_batch_with_backorder_wizard(self):
""" Test a simple batch picking with all quantities fully available but only one fully
processed. The user will set by himself the quantity reserved for each picking and
run the picking batch. There should be a wizard asking for a backorder.
"""
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# Confirm batch, pickings should not be automatically assigned.
self.batch.action_confirm()
self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed')
self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed')
# Ask to assign, so pickings should be assigned now.
self.batch.action_assign()
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
self.picking_client_1.move_ids.quantity_done = 6
self.picking_client_2.move_ids.quantity_done = 10
# There should be a wizard asking to process picking without quantity done
back_order_wizard_dict = self.batch.action_done()
self.assertTrue(back_order_wizard_dict)
back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save()
self.assertEqual(len(back_order_wizard.pick_ids), 1)
back_order_wizard.process()
self.assertEqual(self.picking_client_2.state, 'done', 'Picking 2 should be done')
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
self.assertEqual(self.picking_client_1.move_ids.product_uom_qty, 6, 'initial demand should be 6 after picking split')
self.assertTrue(self.env['stock.picking'].search([('backorder_id', '=', self.picking_client_1.id)]), 'no back order created')
quant_A = self.env['stock.quant']._gather(self.productA, self.stock_location)
quant_B = self.env['stock.quant']._gather(self.productB, self.stock_location)
# ensure that quantity for picking has been moved
self.assertEqual(sum(quant_A.mapped('quantity')), 4)
self.assertFalse(sum(quant_B.mapped('quantity')))
def test_batch_without_backorder_wizard(self):
""" Test a simple batch picking with only one quantity fully available.
The user will set by himself the quantity reserved for each picking and
run the picking batch. There should be a wizard asking for a backorder.
run the picking batch. A backorder should be automatically created.
"""
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
@@ -235,13 +274,12 @@ class TestBatchPicking(TransactionCase):
self.picking_client_1.move_ids.quantity_done = 5
self.picking_client_2.move_ids.quantity_done = 10
self.batch.action_done()
# There should be a wizard asking to process picking without quantity done
back_order_wizard_dict = self.batch.action_done()
self.assertTrue(back_order_wizard_dict)
back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save()
self.assertEqual(len(back_order_wizard.pick_ids), 1)
back_order_wizard.process()
# Checks a backorder was created for `picking_client_1`.
backorder = self.env['stock.picking'].search([('backorder_id', 'in', self.batch.picking_ids.ids)])
self.assertEqual(len(backorder), 1)
self.assertEqual(backorder.backorder_id.id, self.picking_client_1.id)
self.assertEqual(self.picking_client_2.state, 'done', 'Picking 2 should be done')
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
@@ -255,11 +293,11 @@ class TestBatchPicking(TransactionCase):
self.assertFalse(sum(quant_A.mapped('quantity')))
self.assertFalse(sum(quant_B.mapped('quantity')))
def test_batch_with_immediate_transfer_and_backorder_wizard(self):
def test_batch_with_immediate_transfer_and_backorder(self):
""" Test a simple batch picking with only one product fully available.
Everything should be automatically. First one backorder in order to set quantity_done
to reserved quantity. After a second wizard asking for a backorder for the quantity that
has not been fully transfered.
Everything should be automatically. The immediate transfer wizard should asking to set
quantity_done to reserved quantity. A backorder should be created without wizard as the
reserved quantity was fully processed.
"""
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
@@ -278,11 +316,12 @@ class TestBatchPicking(TransactionCase):
self.assertTrue(immediate_transfer_wizard_dict)
immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save()
self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2)
back_order_wizard_dict = immediate_transfer_wizard.process()
self.assertTrue(back_order_wizard_dict)
back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save()
self.assertEqual(len(back_order_wizard.pick_ids), 1)
back_order_wizard.process()
immediate_transfer_wizard.process()
# Checks a backorder was created for `picking_client_1`.
backorder = self.env['stock.picking'].search([('backorder_id', 'in', self.batch.picking_ids.ids)])
self.assertEqual(len(backorder), 1)
self.assertEqual(backorder.backorder_id.id, self.picking_client_1.id)
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
self.assertEqual(self.picking_client_1.move_ids.product_uom_qty, 5, 'initial demand should be 5 after picking split')
@@ -296,12 +335,12 @@ class TestBatchPicking(TransactionCase):
self.assertFalse(sum(quant_B.mapped('quantity')))
def test_batch_with_immediate_transfer_and_backorder_wizard_with_manual_operations(self):
""" Test a simple batch picking with only one quantity fully available.
The user set the quantity done only for the partially available picking.
The test should run the immediate transfer for the first picking and then
the backorder wizard for the second picking.
""" Test a simple batch picking with a quantity not fully processed.
The user set the quantity done only for the first picking.
The test should run the immediate transfer for the second picking and then
the backorder wizard for the first picking.
"""
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0)
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)
# Confirm batch, pickings should not be automatically assigned.
@@ -313,7 +352,7 @@ class TestBatchPicking(TransactionCase):
self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready')
self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready')
self.picking_client_1.move_ids.quantity_done = 5
self.picking_client_1.move_ids.quantity_done = 6
# There should be a wizard asking to make a backorder
back_order_wizard_dict = self.batch.action_done()
self.assertTrue(back_order_wizard_dict)
@@ -324,8 +363,9 @@ class TestBatchPicking(TransactionCase):
back_order_wizard.process()
self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done')
self.assertEqual(self.picking_client_1.move_ids.product_uom_qty, 5, 'initial demand should be 5 after picking split')
self.assertEqual(self.picking_client_1.move_ids.product_uom_qty, 6, 'initial demand should be 5 after picking split')
self.assertFalse(self.picking_client_2.batch_id)
def test_put_in_pack(self):
self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0)
self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0)