[IMP] purchase_stock: propagation of cancel
This commit aims to propagate the cancellation of a purchase order or a request for quotation to the relative stock moves. And thus respect the configuration of the stock rule. Task 1913297 closes odoo/odoo#33803 Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
committed by
William Henrotin
parent
a580538e98
commit
f5ccd36b29
@@ -90,17 +90,19 @@ class PurchaseOrder(models.Model):
|
||||
@api.multi
|
||||
def button_cancel(self):
|
||||
for order in self:
|
||||
for pick in order.picking_ids:
|
||||
if pick.state == 'done':
|
||||
for move in order.order_line.mapped('move_ids'):
|
||||
if move.state == 'done':
|
||||
raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name))
|
||||
# If the product is MTO, change the procure_method of the the closest move to purchase to MTS.
|
||||
# The purpose is to link the po that the user will manually generate to the existing moves's chain.
|
||||
if order.state in ('draft', 'sent', 'to approve'):
|
||||
if order.state in ('draft', 'sent', 'to approve', 'purchase'):
|
||||
for order_line in order.order_line:
|
||||
order_line.move_ids._action_cancel()
|
||||
if order_line.move_dest_ids:
|
||||
move_dest_ids = order_line.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||||
siblings_states = (move_dest_ids.mapped('move_orig_ids')).mapped('state')
|
||||
if all(state in ('done', 'cancel') for state in siblings_states):
|
||||
move_dest_ids = order_line.move_dest_ids
|
||||
if order_line.propagate_cancel:
|
||||
move_dest_ids._action_cancel()
|
||||
else:
|
||||
move_dest_ids.write({'procure_method': 'make_to_stock'})
|
||||
move_dest_ids._recompute_state()
|
||||
|
||||
@@ -229,6 +231,7 @@ class PurchaseOrderLine(models.Model):
|
||||
move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
|
||||
propagate_date = fields.Boolean(string="Propagate Rescheduling", help='The rescheduling is propagated to the next move.')
|
||||
propagate_date_minimum_delta = fields.Integer(string='Reschedule if Higher Than', help='The change must be higher than this value to be propagated')
|
||||
propagate_cancel = fields.Boolean('Propagate cancellation', default=True)
|
||||
|
||||
@api.multi
|
||||
def _compute_qty_received_method(self):
|
||||
@@ -374,6 +377,7 @@ class PurchaseOrderLine(models.Model):
|
||||
'propagate_date': self.propagate_date,
|
||||
'propagate_date_minimum_delta': self.propagate_date_minimum_delta,
|
||||
'description_picking': self.product_id._get_description(self.order_id.picking_type_id),
|
||||
'propagate_cancel': self.propagate_cancel,
|
||||
'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [],
|
||||
'warehouse_id': self.order_id.picking_type_id.warehouse_id.id,
|
||||
}
|
||||
@@ -400,5 +404,5 @@ class PurchaseOrderLine(models.Model):
|
||||
args can be merged. If it returns an empty record then a new line will
|
||||
be created.
|
||||
"""
|
||||
lines = self.filtered(lambda l: l.propagate_date == values['propagate_date'] and l.propagate_date_minimum_delta == values['propagate_date_minimum_delta'])
|
||||
lines = self.filtered(lambda l: l.propagate_date == values['propagate_date'] and l.propagate_date_minimum_delta == values['propagate_date_minimum_delta'] and l.propagate_cancel == values['propagate_cancel'])
|
||||
return lines and lines[0] or self.env['purchase.order.line']
|
||||
|
||||
@@ -53,6 +53,7 @@ class StockRule(models.Model):
|
||||
procurement.values['supplier'] = supplier
|
||||
procurement.values['propagate_date'] = rule.propagate_date
|
||||
procurement.values['propagate_date_minimum_delta'] = rule.propagate_date_minimum_delta
|
||||
procurement.values['propagate_cancel'] = rule.propagate_cancel
|
||||
|
||||
domain = rule._make_po_get_domain(procurement.company_id, procurement.values, partner)
|
||||
procurements_by_po_domain[domain].append((procurement, rule))
|
||||
@@ -117,11 +118,11 @@ class StockRule(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_procurements_to_merge_groupby(self, procurement):
|
||||
return procurement.product_id, procurement.product_uom, procurement.values['propagate_date'], procurement.values['propagate_date_minimum_delta']
|
||||
return procurement.product_id, procurement.product_uom, procurement.values['propagate_date'], procurement.values['propagate_date_minimum_delta'], procurement.values['propagate_cancel']
|
||||
|
||||
@api.model
|
||||
def _get_procurements_to_merge_sorted(self, procurement):
|
||||
return procurement.product_id.id, procurement.product_uom.id, procurement.values['propagate_date'], procurement.values['propagate_date_minimum_delta']
|
||||
return procurement.product_id.id, procurement.product_uom.id, procurement.values['propagate_date'], procurement.values['propagate_date_minimum_delta'], procurement.values['propagate_cancel']
|
||||
|
||||
@api.model
|
||||
def _get_procurements_to_merge(self, procurements):
|
||||
@@ -232,6 +233,7 @@ class StockRule(models.Model):
|
||||
'product_id': product_id.id,
|
||||
'product_uom': product_id.uom_po_id.id,
|
||||
'price_unit': price_unit,
|
||||
'propagate_cancel': values.get('propagate_cancel'),
|
||||
'date_planned': date_planned,
|
||||
'propagate_date': values['propagate_date'],
|
||||
'propagate_date_minimum_delta': values['propagate_date_minimum_delta'],
|
||||
|
||||
@@ -14,3 +14,4 @@ from . import test_purchase_order_process
|
||||
from . import test_stockvaluation
|
||||
from . import test_replenish_wizard
|
||||
from . import test_reordering_rule
|
||||
from . import test_move_cancel_propagation
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests import tagged
|
||||
from .common import TestPurchase
|
||||
|
||||
|
||||
class TestMoveCancelPropagation(TestPurchase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestMoveCancelPropagation, self).setUp()
|
||||
self.customer = self.env['res.partner'].create({'name': 'abc'})
|
||||
self.group = self.env['procurement.group'].create({'partner_id': self.customer.id, 'name': 'New Group'})
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
cust_location = self.env.ref('stock.stock_location_customers')
|
||||
seller = self.env['product.supplierinfo'].create({
|
||||
'name': self.customer.id,
|
||||
'price': 100.0,
|
||||
})
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Geyser',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, self.route_mto), (4, self.route_buy)],
|
||||
'seller_ids': [(6, 0, [seller.id])],
|
||||
})
|
||||
self.picking_out = self.env['stock.picking'].create({
|
||||
'location_id': self.warehouse.out_type_id.default_location_src_id.id,
|
||||
'location_dest_id': cust_location.id,
|
||||
'partner_id': self.customer.id,
|
||||
'group_id': self.group.id,
|
||||
'picking_type_id': self.ref('stock.picking_type_out'),
|
||||
})
|
||||
self.move = self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': self.picking_out.id,
|
||||
'group_id': self.group.id,
|
||||
'location_id': self.warehouse.out_type_id.default_location_src_id.id,
|
||||
'location_dest_id': cust_location.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
|
||||
def test_01_cancel_draft_purchase_order_one_steps(self):
|
||||
""" Check the picking and moves status related PO, When canceling purchase order
|
||||
Ex.
|
||||
1) Set one steps of receiption and delivery on the warehouse.
|
||||
2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
|
||||
3) Cancel 'draft' purchase order should not cancel < Delivery >
|
||||
"""
|
||||
self.warehouse.write({'delivery_steps': 'ship_only', 'reception_steps': 'one_step'})
|
||||
self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.picking_out.action_confirm()
|
||||
|
||||
# Find PO related to picking.
|
||||
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
|
||||
|
||||
# Po should be create related picking.
|
||||
self.assertTrue(purchase_order, 'No purchase order created.')
|
||||
|
||||
# Check status of Purchase Order
|
||||
self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
|
||||
|
||||
# Cancel Purchase order.
|
||||
purchase_order.button_cancel()
|
||||
|
||||
# Check the status of picking after canceling po.
|
||||
self.assertNotEqual(self.picking_out.state, 'cancel')
|
||||
|
||||
def test_02_cancel_confirm_purchase_order_one_steps(self):
|
||||
""" Check the picking and moves status related purchase order, When canceling purchase order
|
||||
after confirming.
|
||||
Ex.
|
||||
1) Set one steps of receiption and delivery on the warehouse.
|
||||
2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
|
||||
3) Cancel 'confirmed' purchase order, should cancel releted < Receiption >
|
||||
but it should not cancel < Delivery > order.
|
||||
"""
|
||||
self.warehouse.write({'delivery_steps': 'ship_only', 'reception_steps': 'one_step'})
|
||||
self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.picking_out.action_confirm()
|
||||
|
||||
# Find PO related to picking.
|
||||
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
|
||||
# Po should be create related picking.
|
||||
self.assertTrue(purchase_order, 'No purchase order created.')
|
||||
|
||||
# Check status of Purchase Order
|
||||
self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
|
||||
purchase_order .button_confirm()
|
||||
picking_in = purchase_order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.in_type_id)
|
||||
# Cancel Purchase order.
|
||||
purchase_order .button_cancel()
|
||||
|
||||
# Check the status of picking after canceling po.
|
||||
self.assertEquals(picking_in.state, 'cancel')
|
||||
self.assertNotEqual(self.picking_out.state, 'cancel')
|
||||
|
||||
def test_03_cancel_draft_purchase_order_two_steps(self):
|
||||
""" Check the picking and moves status related PO, When canceling purchase order
|
||||
in 'draft' state.
|
||||
Ex.
|
||||
1) Set two steps of receiption and delivery on the warehouse.
|
||||
2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
|
||||
3) Cancel 'draft' purchase order should cancel < Input to Stock>
|
||||
but it should not cancel < PICK, Delivery >
|
||||
"""
|
||||
self.warehouse.write({'delivery_steps': 'pick_ship', 'reception_steps': 'two_steps'})
|
||||
self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.picking_out.action_confirm()
|
||||
|
||||
# Find purchase order related to picking.
|
||||
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
|
||||
# Purchase order should be created for picking.
|
||||
self.assertTrue(purchase_order, 'No purchase order created.')
|
||||
|
||||
picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
|
||||
|
||||
internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id and r.group_id.id == self.group.id)
|
||||
pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id and r.group_id.id == self.group.id)
|
||||
|
||||
# Check status of Purchase Order
|
||||
self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
|
||||
# Cancel Purchase order.
|
||||
purchase_order.button_cancel()
|
||||
|
||||
# Check the status of picking after canceling po.
|
||||
for res in internal:
|
||||
self.assertEqual(res.state, 'cancel')
|
||||
self.assertNotEqual(pick.state, 'cancel')
|
||||
self.assertNotEqual(self.picking_out.state, 'cancel')
|
||||
|
||||
def test_04_cancel_confirm_purchase_order_two_steps(self):
|
||||
""" Check the picking and moves status related PO, When canceling purchase order
|
||||
Ex.
|
||||
1) Set 2 steps of receiption and delivery on the warehouse.
|
||||
2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
|
||||
3) Cancel 'comfirm' purchase order should cancel releted < Receiption Picking IN, INT>
|
||||
not < PICK, SHIP >
|
||||
"""
|
||||
self.warehouse.write({'delivery_steps': 'pick_ship', 'reception_steps': 'two_steps'})
|
||||
self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.picking_out.action_confirm()
|
||||
|
||||
# Find PO related to picking.
|
||||
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
|
||||
# Po should be create related picking.
|
||||
self.assertTrue(purchase_order, 'purchase order is created.')
|
||||
|
||||
picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
|
||||
|
||||
internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id)
|
||||
pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id)
|
||||
|
||||
# Check status of Purchase Order
|
||||
self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
|
||||
|
||||
purchase_order.button_confirm()
|
||||
picking_in = purchase_order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.in_type_id)
|
||||
# Cancel Purchase order.
|
||||
purchase_order.button_cancel()
|
||||
|
||||
# Check the status of picking after canceling po.
|
||||
self.assertEqual(picking_in.state, 'cancel')
|
||||
for res in internal:
|
||||
self.assertEqual(res.state, 'cancel')
|
||||
self.assertNotEqual(pick.state, 'cancel')
|
||||
self.assertNotEqual(self.picking_out.state, 'cancel')
|
||||
|
||||
def test_05_cancel_draft_purchase_order_three_steps(self):
|
||||
""" Check the picking and moves status related PO, When canceling purchase order
|
||||
Ex.
|
||||
1) Set 3 steps of receiption and delivery on the warehouse.
|
||||
2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
|
||||
3) Cancel 'draft' purchase order should cancel releted < Receiption Picking IN>
|
||||
not < PICK, PACK, SHIP >
|
||||
"""
|
||||
self.warehouse.write({'delivery_steps': 'pick_pack_ship', 'reception_steps': 'three_steps'})
|
||||
self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.picking_out.action_confirm()
|
||||
|
||||
# Find PO related to picking.
|
||||
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
|
||||
# Po should be create related picking.
|
||||
self.assertTrue(purchase_order, 'No purchase order created.')
|
||||
|
||||
picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
|
||||
|
||||
internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id)
|
||||
pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id)
|
||||
pack = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pack_type_id)
|
||||
|
||||
# Check status of Purchase Order
|
||||
self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
|
||||
# Cancel Purchase order.
|
||||
purchase_order.button_cancel()
|
||||
|
||||
# Check the status of picking after canceling po.
|
||||
for res in internal:
|
||||
self.assertEqual(res.state, 'cancel')
|
||||
self.assertNotEqual(pick.state, 'cancel')
|
||||
self.assertNotEqual(pack.state, 'cancel')
|
||||
self.assertNotEqual(self.picking_out.state, 'cancel')
|
||||
|
||||
def test_06_cancel_confirm_purchase_order_three_steps(self):
|
||||
""" Check the picking and moves status related PO, When canceling purchase order
|
||||
Ex.
|
||||
1) Set 3 steps of receiption and delivery on the warehouse.
|
||||
2) Create Delivery order with mto move and confirm the order, related RFQ should be generated.
|
||||
3) Cancel 'comfirm' purchase order should cancel releted < Receiption Picking IN, INT>
|
||||
not < PICK, PACK, SHIP >
|
||||
"""
|
||||
self.warehouse.write({'delivery_steps': 'pick_pack_ship', 'reception_steps': 'three_steps'})
|
||||
self.picking_out.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.move.write({'location_id': self.warehouse.out_type_id.default_location_src_id.id})
|
||||
self.picking_out.action_confirm()
|
||||
|
||||
# Find PO related to picking.
|
||||
purchase_order = self.env['purchase.order'].search([('partner_id', '=', self.customer.id)])
|
||||
# Po should be create related picking.
|
||||
self.assertTrue(purchase_order, 'No purchase order created.')
|
||||
|
||||
picking_ids = self.env['stock.picking'].search([('group_id', '=', self.group.id)])
|
||||
|
||||
internal = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.int_type_id)
|
||||
pick = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pick_type_id)
|
||||
pack = picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.pack_type_id)
|
||||
|
||||
# Check status of Purchase Order
|
||||
self.assertEqual(purchase_order.state, 'draft', "Purchase order should be in 'draft' state.")
|
||||
|
||||
purchase_order.button_confirm()
|
||||
picking_in = purchase_order.picking_ids.filtered(lambda r: r.picking_type_id == self.warehouse.in_type_id)
|
||||
# Cancel Purchase order.
|
||||
purchase_order.button_cancel()
|
||||
|
||||
# Check the status of picking after canceling po.
|
||||
self.assertEqual(picking_in.state, 'cancel')
|
||||
for res in internal:
|
||||
self.assertEqual(res.state, 'cancel')
|
||||
self.assertNotEqual(pick.state, 'cancel')
|
||||
self.assertNotEqual(pack.state, 'cancel')
|
||||
self.assertNotEqual(self.picking_out.state, 'cancel')
|
||||
@@ -27,6 +27,9 @@
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='invoice_lines']" position="after">
|
||||
<field name="move_ids"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='account_analytic_id']" position="before">
|
||||
<field name="propagate_cancel" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='order_line']/tree//field[@name='qty_received']" position="attributes">
|
||||
<attribute name="attrs">{'column_invisible': [('parent.state', 'not in', ('purchase', 'done'))], 'readonly': [('product_type', 'in', ('consu', 'product'))]}</attribute>
|
||||
</xpath>
|
||||
|
||||
Reference in New Issue
Block a user