From 77b283f30711738c29de9a961835b5346034784f Mon Sep 17 00:00:00 2001 From: Haresh Shyara Date: Tue, 28 May 2019 05:04:21 +0000 Subject: [PATCH] [REF] purchase_stock: rescheduling propagation Implement the propagation minimum delta per rule in purchase_stock. Override properly the write to handle both RFQ and PO. Take care of not merging po lines where the expecte date should be different Add propre test cases. task-1913401 --- addons/purchase_stock/models/purchase.py | 25 +++-- addons/purchase_stock/models/stock_rule.py | 8 +- .../tests/test_create_picking.py | 94 +++++++++++++++- .../tests/test_purchase_lead_time.py | 103 ++++++++++++++++++ 4 files changed, 220 insertions(+), 10 deletions(-) diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py index f03a4391990..83405a9dad9 100644 --- a/addons/purchase_stock/models/purchase.py +++ b/addons/purchase_stock/models/purchase.py @@ -3,7 +3,7 @@ from odoo import api, fields, models, _ from odoo.tools.float_utils import float_compare - +from dateutil import relativedelta from odoo.exceptions import UserError from odoo.addons.purchase.models.purchase import PurchaseOrder as Purchase @@ -227,6 +227,8 @@ class PurchaseOrderLine(models.Model): move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False) orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint') 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') @api.multi def _compute_qty_received_method(self): @@ -266,12 +268,18 @@ class PurchaseOrderLine(models.Model): @api.multi def write(self, values): + for line in self: + if values.get('date_planned') and line.propagate_date: + new_date = values['date_planned'] + delta_days = (new_date - line.date_planned).total_seconds() / 86400 + if abs(delta_days) < line.propagate_date_minimum_delta: + continue + moves_to_update = line.move_ids.filtered(lambda m: m.state not in ('done', 'cancel')) + if not moves_to_update: + moves_to_update = line.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel')) + for move in moves_to_update: + move.date_expected = move.date_expected + relativedelta.relativedelta(days=delta_days) result = super(PurchaseOrderLine, self).write(values) - # Update expected date of corresponding moves - if 'date_planned' in values: - self.env['stock.move'].search([ - ('purchase_line_id', 'in', self.ids), ('state', '!=', 'done') - ]).write({'date_expected': values['date_planned']}) if 'product_qty' in values: self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking() return result @@ -361,6 +369,8 @@ class PurchaseOrderLine(models.Model): 'picking_type_id': self.order_id.picking_type_id.id, 'group_id': self.order_id.group_id.id, 'origin': self.order_id.name, + '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), '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, @@ -388,4 +398,5 @@ class PurchaseOrderLine(models.Model): args can be merged. If it returns an empty record then a new line will be created. """ - return self and self[0] or self.env['purchase.order.line'] + lines = self.filtered(lambda l: l.propagate_date == values['propagate_date'] and l.propagate_date_minimum_delta == values['propagate_date_minimum_delta']) + 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 b6f633b5370..11a370800b1 100644 --- a/addons/purchase_stock/models/stock_rule.py +++ b/addons/purchase_stock/models/stock_rule.py @@ -51,6 +51,8 @@ class StockRule(models.Model): partner = supplier.name # we put `supplier_info` in values for extensibility purposes procurement.values['supplier'] = supplier + procurement.values['propagate_date'] = rule.propagate_date + procurement.values['propagate_date_minimum_delta'] = rule.propagate_date_minimum_delta domain = rule._make_po_get_domain(procurement.company_id, procurement.values, partner) procurements_by_po_domain[domain].append((procurement, rule)) @@ -115,11 +117,11 @@ class StockRule(models.Model): @api.model def _get_procurements_to_merge_groupby(self, procurement): - return procurement.product_id, procurement.product_uom + return procurement.product_id, procurement.product_uom, procurement.values['propagate_date'], procurement.values['propagate_date_minimum_delta'] @api.model def _get_procurements_to_merge_sorted(self, procurement): - return procurement.product_id.id, procurement.product_uom.id + return procurement.product_id.id, procurement.product_uom.id, procurement.values['propagate_date'], procurement.values['propagate_date_minimum_delta'] @api.model def _get_procurements_to_merge(self, procurements): @@ -231,6 +233,8 @@ class StockRule(models.Model): 'product_uom': product_id.uom_po_id.id, 'price_unit': price_unit, 'date_planned': date_planned, + 'propagate_date': values['propagate_date'], + 'propagate_date_minimum_delta': values['propagate_date_minimum_delta'], 'orderpoint_id': values.get('orderpoint_id', False) and values.get('orderpoint_id').id, 'taxes_id': [(6, 0, taxes_id.ids)], 'order_id': po.id, diff --git a/addons/purchase_stock/tests/test_create_picking.py b/addons/purchase_stock/tests/test_create_picking.py index d8a9a37fef6..18b7369a697 100644 --- a/addons/purchase_stock/tests/test_create_picking.py +++ b/addons/purchase_stock/tests/test_create_picking.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime +from datetime import datetime, timedelta from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from odoo.addons.product.tests import common @@ -313,3 +313,95 @@ class TestCreatePicking(common.TestProductCommon): purchase_order_2.picking_ids.button_validate() self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.') + + def create_delivery_order(self, propagate_date, propagate_date_minimum_delta): + stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock') + customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers') + unit = self.ref("uom.product_uom_unit") + picking_type_out = self.env['ir.model.data'].xmlid_to_object('stock.picking_type_out') + partner = self.env['res.partner'].create({'name': 'AAA', 'email': 'from.test@example.com'}) + supplier_info1 = self.env['product.supplierinfo'].create({ + 'name': partner.id, + 'price': 50, + }) + + warehouse1 = self.env.ref('stock.warehouse0') + route_buy = warehouse1.buy_pull_id.route_id + route_mto = warehouse1.mto_pull_id.route_id + + # change propagete date and Reschedule if Higher Than on rules + route_buy.rule_ids.write({'propagate_date': propagate_date, + 'propagate_date_minimum_delta': propagate_date_minimum_delta}) + + product = self.env['product.product'].create({ + 'name': 'Usb Keyboard', + 'type': 'product', + 'uom_id': unit, + 'uom_po_id': unit, + 'seller_ids': [(6, 0, [supplier_info1.id])], + 'route_ids': [(6, 0, [route_buy.id, route_mto.id])] + }) + + delivery_order = self.env['stock.picking'].create({ + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + 'partner_id': partner.id, + 'picking_type_id': picking_type_out.id, + }) + + customer_move = self.env['stock.move'].create({ + 'name': 'move out', + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + 'product_id': product.id, + 'product_uom': product.uom_id.id, + 'product_uom_qty': 10.0, + 'procure_method': 'make_to_order', + 'picking_id': delivery_order.id, + }) + + customer_move._action_confirm() + # find created po the product + purchase_order = self.env['purchase.order'].search([('partner_id', '=', partner.id)]) + + return delivery_order, purchase_order + + def test_05_if_propagate_date(self): + """ In order to check scheduled date of the delivery order is changed based + stock rules with propagate date and minimum delta. + """ + + # Create Delivery Order and with propagate date and minimum delta + delivery_order, purchase_order = self.create_delivery_order(True, 5) + + # check po is created or not + self.assertTrue(purchase_order, 'No purchase order created.') + + purchase_order_line = purchase_order.order_line + + # change scheduled date of po line. + purchase_order_line.write({'date_planned': purchase_order_line.date_planned + timedelta(days=5)}) + + # Now check scheduled date of delivery order is changed or not. + self.assertEquals(purchase_order_line.date_planned, delivery_order.scheduled_date, + 'Delivery order schedule date should be changed as we have set date propagate.') + + def test_06_no_propagate_date(self): + """ In order to check scheduled date of the delivery order is changed based + stock rules without propagate date and minimum delta. + """ + + # Create Delivery Order and without propagate date and minimum delta + delivery_order, purchase_order = self.create_delivery_order(False, 5) + + # check po is created or not + self.assertTrue(purchase_order, 'No purchase order created.') + + purchase_order_line = purchase_order.order_line + + # change scheduled date of po line. + purchase_order_line.write({'date_planned': purchase_order_line.date_planned + timedelta(days=5)}) + + # Now check scheduled date of delivery order is changed or not. + self.assertNotEquals(purchase_order_line.date_planned, delivery_order.scheduled_date, + 'Delivery order schedule date should not changed.') diff --git a/addons/purchase_stock/tests/test_purchase_lead_time.py b/addons/purchase_stock/tests/test_purchase_lead_time.py index f14c3dc9df1..bce67d23eea 100644 --- a/addons/purchase_stock/tests/test_purchase_lead_time.py +++ b/addons/purchase_stock/tests/test_purchase_lead_time.py @@ -136,3 +136,106 @@ class TestPurchaseLeadTime(TestPurchase): incoming_shipment2 = self.env['stock.picking'].search([('picking_type_id', '=', self.warehouse_1.int_type_id.id), ('location_id', '=', self.warehouse_1.wh_qc_stock_loc_id.id), ('location_dest_id', '=', self.warehouse_1.lot_stock_id.id)]) incoming_shipment2_date = schedule_date - timedelta(days=incoming_shipment2.move_lines[0].rule_id.delay) self.assertEqual(incoming_shipment2.scheduled_date, incoming_shipment2_date, 'Schedule date of Internal Type shipment for quality control stock location should be equal to: schedule date of Internal type shipment for input stock location + push rule delay..') + + def test_merge_po_line(self): + """Chage that merging po line for same procurement is done depending on + propagate_date and propagate_date_minimum_delta""" + + # create a product with manufacture route + product_1 = self.env['product.product'].create({ + 'name': 'AAA', + 'route_ids': [(4, self.route_buy)], + 'seller_ids': [(0, 0, {'name': self.partner_1.id, 'delay': 5})] + }) + + # create a move for product_1 from stock to output and reserve to trigger the + # rule + move_1 = self.env['stock.move'].create({ + 'name': 'move_1', + 'product_id': product_1.id, + 'product_uom': self.ref('uom.product_uom_unit'), + 'propagate_date': True, + 'propagate_date_minimum_delta': 1, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + 'product_uom_qty': 10, + 'procure_method': 'make_to_order' + }) + + move_1._action_confirm() + po_line = self.env['purchase.order.line'].search([ + ('product_id', '=', product_1.id), + ]) + self.assertEqual(len(po_line), 1, 'the purchase order line is not created') + self.assertEqual(po_line.product_qty, 10, 'the purchase order line has a wrong quantity') + + move_2 = self.env['stock.move'].create({ + 'name': 'move_2', + 'product_id': product_1.id, + 'product_uom': self.ref('uom.product_uom_unit'), + 'propagate_date': True, + 'propagate_date_minimum_delta': 1, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + 'product_uom_qty': 5, + 'procure_method': 'make_to_order' + }) + + move_2._action_confirm() + po_line = self.env['purchase.order.line'].search([ + ('product_id', '=', product_1.id), + ]) + self.assertEqual(len(po_line), 1, 'the purchase order lines should be merged') + self.assertEqual(po_line.product_qty, 15, 'the purchase order line has a wrong quantity') + + def test_merge_po_line_2(self): + """Chage that merging po line for same procurement is done depending on + propagate_date and propagate_date_minimum_delta""" + + # create a product with manufacture route + product_1 = self.env['product.product'].create({ + 'name': 'AAA', + 'route_ids': [(4, self.route_buy)], + 'seller_ids': [(0, 0, {'name': self.partner_1.id, 'delay': 5})] + }) + + # create a move for product_1 from stock to output and reserve to trigger the + # rule + move_1 = self.env['stock.move'].create({ + 'name': 'move_1', + 'product_id': product_1.id, + 'product_uom': self.ref('uom.product_uom_unit'), + 'propagate_date': True, + 'propagate_date_minimum_delta': 1, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + 'product_uom_qty': 10, + 'procure_method': 'make_to_order' + }) + + move_1._action_confirm() + po_line = self.env['purchase.order.line'].search([ + ('product_id', '=', product_1.id), + ]) + self.assertEqual(len(po_line), 1, 'the purchase order line is not created') + self.assertEqual(po_line.product_qty, 10, 'the purchase order line has a wrong quantity') + po_line.propagate_date = not po_line.propagate_date + move_2 = self.env['stock.move'].create({ + 'name': 'move_2', + 'product_id': product_1.id, + 'product_uom': self.ref('uom.product_uom_unit'), + 'propagate_date': True, + 'propagate_date_minimum_delta': 1, + 'location_id': self.ref('stock.stock_location_stock'), + 'location_dest_id': self.ref('stock.stock_location_output'), + 'product_uom_qty': 5, + 'procure_method': 'make_to_order' + }) + + move_2._action_confirm() + po_line = self.env['purchase.order.line'].search([ + ('product_id', '=', product_1.id), + ]) + self.assertEqual(len(po_line), 2, 'the purchase order lines are not merged') + self.assertEqual(po_line[0].product_qty, 10, 'the purchase order line has a wrong quantity') + self.assertEqual(po_line[1].product_qty, 5, 'the purchase order line has a wrong quantity')