diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py
index 077ab49487d..314c5655d2a 100644
--- a/addons/purchase_stock/models/purchase.py
+++ b/addons/purchase_stock/models/purchase.py
@@ -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']
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index 6a0f9bf518a..885e8990ad7 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -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'],
diff --git a/addons/purchase_stock/tests/__init__.py b/addons/purchase_stock/tests/__init__.py
index 46efcec099d..3deb9f79a75 100644
--- a/addons/purchase_stock/tests/__init__.py
+++ b/addons/purchase_stock/tests/__init__.py
@@ -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
diff --git a/addons/purchase_stock/tests/test_move_cancel_propagation.py b/addons/purchase_stock/tests/test_move_cancel_propagation.py
new file mode 100644
index 00000000000..2c7aeda0719
--- /dev/null
+++ b/addons/purchase_stock/tests/test_move_cancel_propagation.py
@@ -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')
diff --git a/addons/purchase_stock/views/purchase_views.xml b/addons/purchase_stock/views/purchase_views.xml
index 92330f01d22..947ba72b686 100644
--- a/addons/purchase_stock/views/purchase_views.xml
+++ b/addons/purchase_stock/views/purchase_views.xml
@@ -27,6 +27,9 @@
+
+
+
{'column_invisible': [('parent.state', 'not in', ('purchase', 'done'))], 'readonly': [('product_type', 'in', ('consu', 'product'))]}