[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:
Dhaval Panchal
2019-06-11 07:57:04 +00:00
committed by William Henrotin
parent a580538e98
commit f5ccd36b29
5 changed files with 267 additions and 9 deletions
+11 -7
View File
@@ -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']
+4 -2
View File
@@ -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'],
+1
View File
@@ -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>