From a3a6992ba7aa3e6003b915c784ae1e2e3eaa5e04 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Wed, 13 Jun 2018 20:16:40 +0200 Subject: [PATCH] [11.0][REW/IMP] multi_level_mrp: major overhault --- multi_level_mrp/models/mrp_forecast.py | 2 +- multi_level_mrp/models/mrp_inventory.py | 3 +- multi_level_mrp/models/mrp_move.py | 89 +-- multi_level_mrp/models/mrp_product.py | 106 +++- multi_level_mrp/models/product.py | 18 +- multi_level_mrp/tests/test_multi_level_mrp.py | 251 ++++++-- multi_level_mrp/views/mrp_product_view.xml | 2 +- multi_level_mrp/wizards/multi_level_mrp.py | 562 +++++++++--------- 8 files changed, 640 insertions(+), 393 deletions(-) diff --git a/multi_level_mrp/models/mrp_forecast.py b/multi_level_mrp/models/mrp_forecast.py index 3bb9b0fd6..b7787d022 100644 --- a/multi_level_mrp/models/mrp_forecast.py +++ b/multi_level_mrp/models/mrp_forecast.py @@ -175,7 +175,7 @@ class MrpForecastProduct(models.Model): 'forecast_product_id', 'Forecast') name = fields.Char(compute='_function_name', string='Description') - product_id = fields.Many2one('product.product', 'Product', select=True) + product_id = fields.Many2one('product.product', 'Product', index=True) mrp_area_id = fields.Many2one('mrp.area', 'MRP Area') qty_forecast_m0 = fields.Float(compute='_function_forecast_m0', string='This Month Forecast') diff --git a/multi_level_mrp/models/mrp_inventory.py b/multi_level_mrp/models/mrp_inventory.py index 29029a6d3..d0bf1be91 100644 --- a/multi_level_mrp/models/mrp_inventory.py +++ b/multi_level_mrp/models/mrp_inventory.py @@ -17,6 +17,7 @@ class MrpInventory(models.Model): # TODO: compute procurement_date to pass to the wizard? not needed for PO at least. Check for MO and moves # TODO: substract qty already procured. # TODO: show a LT based on the procure method? + # TODO: add to_procure_date mrp_area_id = fields.Many2one( comodel_name='mrp.area', string='MRP Area', @@ -24,7 +25,7 @@ class MrpInventory(models.Model): ) mrp_product_id = fields.Many2one( comodel_name='mrp.product', string='Product', - select=True, + index=True, ) uom_id = fields.Many2one( comodel_name='product.uom', string='Product UoM', diff --git a/multi_level_mrp/models/mrp_move.py b/multi_level_mrp/models/mrp_move.py index 511f7a70c..126d08741 100644 --- a/multi_level_mrp/models/mrp_move.py +++ b/multi_level_mrp/models/mrp_move.py @@ -1,6 +1,5 @@ # © 2016 Ucamco - Wim Audenaert -# © 2016 Eficent Business and IT Consulting Services S.L. -# - Jordi Ballester Alomar +# © 2016-18 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import models, fields, api, _ @@ -9,43 +8,66 @@ from odoo import exceptions class MrpMove(models.Model): _name = 'mrp.move' - + _order = 'mrp_product_id, mrp_date, mrp_type desc, id' + + # TODO: too many indexes... + mrp_area_id = fields.Many2one('mrp.area', 'MRP Area') current_date = fields.Date('Current Date') current_qty = fields.Float('Current Qty') - mrp_action = fields.Selection((('mo', 'Manufacturing Order'), - ('po', 'Purchase Order'), - ('pr', 'Purchase Request'), - ('so', 'Sale Order'), - ('push', 'Push'), - ('pull', 'Pull'), - ('cancel', 'Cancel'), - ('none', 'None')), - 'Action') + # TODO: remove purchase request and move to other module? + # TODO: cancel is not needed I think... + mrp_action = fields.Selection( + selection=[('mo', 'Manufacturing Order'), + ('po', 'Purchase Order'), + ('pr', 'Purchase Request'), + ('so', 'Sale Order'), + ('cancel', 'Cancel'), + ('none', 'None')], + string='Action', + ) mrp_action_date = fields.Date('MRP Action Date') mrp_date = fields.Date('MRP Date') - mrp_move_down_ids = fields.Many2many('mrp.move', 'mrp_move_rel', - 'move_up_id', 'move_down_id', - 'MRP Move DOWN') - mrp_move_up_ids = fields.Many2many('mrp.move', 'mrp_move_rel', - 'move_down_id', 'move_up_id', - 'MRP Move UP') - mrp_minimum_stock = fields.Float(string='Minimum Stock', - related='product_id.mrp_minimum_stock') + mrp_move_down_ids = fields.Many2many( + comodel_name='mrp.move', + relation='mrp_move_rel', + column1='move_up_id', + column2='move_down_id', + string='MRP Move DOWN', + ) + mrp_move_up_ids = fields.Many2many( + comodel_name='mrp.move', + relation='mrp_move_rel', + column1='move_down_id', + column2='move_up_id', + string='MRP Move UP', + ) + mrp_minimum_stock = fields.Float( + string='Minimum Stock', + related='product_id.mrp_minimum_stock', + ) mrp_order_number = fields.Char('Order Number') - mrp_origin = fields.Selection((('mo', 'Manufacturing Order'), - ('po', 'Purchase Order'), - ('pr', 'Purchase Request'), - ('so', 'Sale Order'), ('mv','Move'), - ('fc', 'Forecast'), ('mrp', 'MRP')), - 'Origin') + # TODO: move purchase request to another module + mrp_origin = fields.Selection( + selection=[('mo', 'Manufacturing Order'), + ('po', 'Purchase Order'), + ('pr', 'Purchase Request'), + ('so', 'Sale Order'), + ('mv', 'Move'), + ('fc', 'Forecast'), ('mrp', 'MRP')], + string='Origin') mrp_processed = fields.Boolean('Processed') mrp_product_id = fields.Many2one('mrp.product', 'Product', index=True) mrp_qty = fields.Float('MRP Quantity') - mrp_type = fields.Selection((('s', 'Supply'), ('d', 'Demand')), 'Type') + mrp_type = fields.Selection( + selection=[('s', 'Supply'), ('d', 'Demand')], + string='Type', + ) name = fields.Char('Description') - parent_product_id = fields.Many2one('product.product', - 'Parent Product', index=True) + parent_product_id = fields.Many2one( + comodel_name='product.product', + string='Parent Product', index=True, + ) product_id = fields.Many2one('product.product', 'Product', index=True) production_id = fields.Many2one('mrp.production', @@ -65,15 +87,12 @@ class MrpMove(models.Model): ('waiting', 'Waiting'), ('partially_available', 'Partially Available'), ('ready', 'Ready'), - ('in_production', 'In Production'), - ('picking_except', 'Picking Exception'), - ('sent', 'Sent'), ('approved', 'Approved'), - ('except_invoice', 'Invoice Exception')], + ('sent', 'Sent'), + ('to approve', 'To Approve'), + ('approved', 'Approved')], string='State', ) stock_move_id = fields.Many2one('stock.move', 'Stock Move', index=True) - - _order = 'mrp_product_id, mrp_date, mrp_type desc, id' @api.model def mrp_production_prepare(self, bom_id, routing_id): diff --git a/multi_level_mrp/models/mrp_product.py b/multi_level_mrp/models/mrp_product.py index 37cf6ee4f..3e44a525f 100644 --- a/multi_level_mrp/models/mrp_product.py +++ b/multi_level_mrp/models/mrp_product.py @@ -1,9 +1,8 @@ # © 2016 Ucamco - Wim Audenaert -# © 2016 Eficent Business and IT Consulting Services S.L. -# - Jordi Ballester Alomar +# © 2016-18 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from odoo import fields, models +from odoo import api, fields, models class MrpProduct(models.Model): @@ -12,38 +11,105 @@ class MrpProduct(models.Model): mrp_area_id = fields.Many2one('mrp.area', 'MRP Area') current_qty_available = fields.Float(string='Current Qty Available', related='product_id.qty_available') - main_supplier_id = fields.Many2one('res.partner', 'Main Supplier', - select=True) + main_supplier_id = fields.Many2one( + comodel_name='res.partner', string='Main Supplier', + compute='_compute_main_supplier', store=True, + index=True, + ) mrp_inspection_delay = fields.Integer( string='Inspection Delay', related='product_id.mrp_inspection_delay') - mrp_lead_time = fields.Float(string='Lead Time', - related='product_id.produce_delay') - mrp_llc = fields.Integer('Low Level Code', select=True) + mrp_lead_time = fields.Float( + string='Lead Time', + related='product_id.produce_delay', + ) + mrp_llc = fields.Integer( + string='Low Level Code', index=True, readonly=True, + ) + # TODO: minimun stock and max/min order qty assigned by area? mrp_maximum_order_qty = fields.Float( - string='Maximum Order Qty', related='product_id.mrp_maximum_order_qty') + string='Maximum Order Qty', related='product_id.mrp_maximum_order_qty', + ) mrp_minimum_order_qty = fields.Float( - string='Minimum Order Qty', related='product_id.mrp_minimum_order_qty') - mrp_minimum_stock = fields.Float(string='Minimum Stock', - related='product_id.mrp_minimum_stock') - mrp_move_ids = fields.One2many('mrp.move', 'mrp_product_id', 'MRP Moves') + string='Minimum Order Qty', related='product_id.mrp_minimum_order_qty', + ) + mrp_minimum_stock = fields.Float( + string='Minimum Stock', + related='product_id.mrp_minimum_stock', + ) + mrp_move_ids = fields.One2many( + comodel_name='mrp.move', inverse_name='mrp_product_id', + string='MRP Moves', + ) mrp_nbr_days = fields.Integer( string='Nbr. Days', related='product_id.mrp_nbr_days') mrp_qty_available = fields.Float('MRP Qty Available') mrp_qty_multiple = fields.Float(string='Qty Multiple', related='product_id.mrp_qty_multiple') # TODO: this was: mrp_transit_delay = fields.Integer(mrp_move_ids) ??¿?¿¿? - mrp_transit_delay = fields.Integer(related = 'product_id.mrp_transit_delay') + mrp_transit_delay = fields.Integer(related='product_id.mrp_transit_delay') mrp_verified = fields.Boolean(string='MRP Verified', related='product_id.mrp_verified') name = fields.Char('Description') - nbr_mrp_actions = fields.Integer('Nbr Actions', select=True) - nbr_mrp_actions_4w = fields.Integer('Nbr Actions 4 Weeks', select=True) - product_id = fields.Many2one('product.product', 'Product', select=True) + # TODO: rename to mrp_action_count? + nbr_mrp_actions = fields.Integer( + string='Nbr Actions', index=True, + ) + nbr_mrp_actions_4w = fields.Integer( + string='Nbr Actions 4 Weeks', index=True, + ) + product_id = fields.Many2one( + comodel_name='product.product', string='Product', + index=True, + ) product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id') # TODO: extension to purchase requisition in other module? # purchase_requisition = fields.Boolean(string='Purchase Requisition', # related='product_id.purchase_requisition') - supply_method = fields.Selection((('buy', 'Buy'), - ('produce', 'Produce')), - 'Supply Method') + supply_method = fields.Selection( + selection=[('buy', 'Buy'), + ('none', 'Undefined'), + ('manufacture', 'Produce'), + ('move', 'Transfer')], + string='Supply Method', + compute='_compute_supply_method', store=True, + ) + + @api.multi + @api.depends('mrp_area_id') + def _compute_supply_method(self): + group_obj = self.env['procurement.group'] + for rec in self: + values = { + 'warehouse_id': rec.mrp_area_id.warehouse_id, + 'company_id': self.env.user.company_id.id, # TODO: better way to get company + } + rule = group_obj._get_rule( + rec.product_id, rec.mrp_area_id.location_id, values) + rec.supply_method = rule.action if rule else 'none' + + @api.multi + @api.depends('supply_method') + def _compute_main_supplier(self): + """Simplified and similar to procurement.rule logic.""" + for rec in self.filtered(lambda r: r.supply_method == 'buy'): + suppliers = rec.product_id.seller_ids.filtered( + lambda r: (not r.product_id or r.product_id == rec.product_id)) + if not suppliers: + continue + rec.main_supplier_id = suppliers[0].name + + @api.multi + def _adjust_qty_to_order(self, qty_to_order): + # TODO: consider mrp_qty_multiple? + self.ensure_one() + if not self.mrp_maximum_order_qty and not self.mrp_minimum_order_qty: + return qty_to_order + if qty_to_order < self.mrp_minimum_order_qty: + return self.mrp_minimum_order_qty + if self.mrp_maximum_order_qty and qty_to_order > \ + self.mrp_maximum_order_qty: + qty = self.mrp_maximum_order_qty + else: + qty = qty_to_order + return qty diff --git a/multi_level_mrp/models/product.py b/multi_level_mrp/models/product.py index 95007bc23..21eb147b6 100644 --- a/multi_level_mrp/models/product.py +++ b/multi_level_mrp/models/product.py @@ -19,14 +19,18 @@ class Product(models.Model): mrp_applicable = fields.Boolean('MRP Applicable') mrp_exclude = fields.Boolean('Exclude from MRP') mrp_inspection_delay = fields.Integer('Inspection Delay', default=0) - mrp_maximum_order_qty = fields.Float('Maximum Order Qty', default=0.00) - mrp_minimum_order_qty = fields.Float('Minimum Order Qty', default=0.00) + mrp_maximum_order_qty = fields.Float( + string='Maximum Order Qty', default=0.0, + ) + mrp_minimum_order_qty = fields.Float( + string='Minimum Order Qty', default=0.0, + ) mrp_minimum_stock = fields.Float('Minimum Stock') - mrp_nbr_days = fields.Integer('Nbr. Days', default=0, - help="Number of days to group demand for " - "this product during the MRP run, " - "in order to determine the quantity " - "to order.") + mrp_nbr_days = fields.Integer( + string='Nbr. Days', default=0, + help="Number of days to group demand for this product during the " + "MRP run, in order to determine the quantity to order.", + ) mrp_product_ids = fields.One2many('mrp.product', 'product_id', 'MRP Product data') mrp_qty_multiple = fields.Float('Qty Multiple', default=1.00) diff --git a/multi_level_mrp/tests/test_multi_level_mrp.py b/multi_level_mrp/tests/test_multi_level_mrp.py index ee801aae9..f880d370f 100644 --- a/multi_level_mrp/tests/test_multi_level_mrp.py +++ b/multi_level_mrp/tests/test_multi_level_mrp.py @@ -2,7 +2,7 @@ # (http://www.eficent.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from odoo.tests.common import SavepointCase from odoo import fields @@ -13,27 +13,36 @@ class TestMultiLevelMRP(SavepointCase): @classmethod def setUpClass(cls): super(TestMultiLevelMRP, cls).setUpClass() - cls.wiz_multi_level_mrp_model = cls.env['multi.level.mrp'] - cls.stock_picking_model = cls.env['stock.picking'] - cls.mrp_inventory_model = cls.env['mrp.inventory'] + cls.mo_obj = cls.env['mrp.production'] + cls.po_obj = cls.env['purchase.order'] + cls.stock_picking_obj = cls.env['stock.picking'] + cls.multi_level_mrp_wiz = cls.env['multi.level.mrp'] + cls.mrp_inventory_procure_wiz = cls.env['mrp.inventory.procure'] + cls.mrp_inventory_obj = cls.env['mrp.inventory'] + cls.mrp_product_obj = cls.env['mrp.product'] + cls.mrp_move_obj = cls.env['mrp.move'] + cls.fp_1 = cls.env.ref('multi_level_mrp.product_product_fp_1') cls.fp_2 = cls.env.ref('multi_level_mrp.product_product_fp_2') cls.sf_1 = cls.env.ref('multi_level_mrp.product_product_sf_1') cls.sf_2 = cls.env.ref('multi_level_mrp.product_product_sf_2') cls.pp_1 = cls.env.ref('multi_level_mrp.product_product_pp_1') cls.pp_2 = cls.env.ref('multi_level_mrp.product_product_pp_2') + cls.vendor = cls.env.ref('multi_level_mrp.res_partner_lazer_tech') cls.wh = cls.env.ref('stock.warehouse0') cls.stock_location = cls.wh.lot_stock_id cls.customer_location = cls.env.ref( 'stock.stock_location_customers') + + # Create test picking: date_move = datetime.today() + timedelta(days=7) - cls.picking_1 = cls.stock_picking_model.create({ + cls.picking_1 = cls.stock_picking_obj.create({ 'picking_type_id': cls.env.ref('stock.picking_type_out').id, 'location_id': cls.stock_location.id, 'location_dest_id': cls.customer_location.id, 'move_lines': [ (0, 0, { - 'name': 'Test move pf-1', + 'name': 'Test move fp-1', 'product_id': cls.fp_1.id, 'date_expected': date_move, 'date': date_move, @@ -54,7 +63,44 @@ class TestMultiLevelMRP(SavepointCase): })] }) cls.picking_1.action_confirm() - cls.wiz_multi_level_mrp_model.create({}).run_multi_level_mrp() + + # Create Test PO: + date_po = datetime.today() + timedelta(days=1) + cls.po = cls.po_obj.create({ + 'name': 'Test PO-001', + 'partner_id': cls.vendor.id, + 'order_line': [ + (0, 0, { + 'name': 'Test PP-2 line', + 'product_id': cls.pp_2.id, + 'date_planned': date_po, + 'product_qty': 5.0, + 'product_uom': cls.pp_2.uom_id.id, + 'price_unit': 25.0, + })], + }) + + # Create test MO: + date_mo = datetime.today() + timedelta(days=9) + bom_fp_2 = cls.env.ref('multi_level_mrp.mrp_bom_fp_2') + cls.mo = cls.mo_obj.create({ + 'product_id': cls.fp_2.id, + 'bom_id': bom_fp_2.id, + 'product_qty': 12.0, + 'product_uom_id': cls.fp_2.uom_id.id, + 'date_planned_start': date_mo, + }) + cls.multi_level_mrp_wiz.create({}).run_multi_level_mrp() + + # Dates (Strings): + today = datetime.today() + cls.date_3 = fields.Date.to_string(today + timedelta(days=3)) + cls.date_5 = fields.Date.to_string(today + timedelta(days=5)) + cls.date_6 = fields.Date.to_string(today + timedelta(days=6)) + cls.date_7 = fields.Date.to_string(today + timedelta(days=7)) + cls.date_8 = fields.Date.to_string(today + timedelta(days=8)) + cls.date_9 = fields.Date.to_string(today + timedelta(days=9)) + cls.date_10 = fields.Date.to_string(today + timedelta(days=10)) def test_01_mrp_levels(self): """Tests computation of MRP levels.""" @@ -65,61 +111,172 @@ class TestMultiLevelMRP(SavepointCase): self.assertEqual(self.pp_1.llc, 2) self.assertEqual(self.pp_2.llc, 2) - def test_02_multi_level_mrp(self): + def test_02_mrp_product(self): + """Tests that mrp products are generated correctly.""" + mrp_product = self.mrp_product_obj.search([ + ('product_id', '=', self.pp_1.id)]) + self.assertEqual(mrp_product.supply_method, 'buy') + self.assertEqual(mrp_product.main_supplier_id, self.vendor) + self.assertEqual(mrp_product.mrp_qty_available, 10.0) + mrp_product = self.mrp_product_obj.search([ + ('product_id', '=', self.sf_1.id)]) + self.assertEqual(mrp_product.supply_method, 'manufacture') + + def test_03_mrp_moves(self): + """Tests for mrp moves generated.""" + moves = self.mrp_move_obj.search([ + ('product_id', '=', self.pp_1.id), + ('mrp_action', '=', 'none'), + ]) + self.assertEqual(len(moves), 3) + self.assertNotIn('s', moves.mapped('mrp_type')) + for move in moves: + self.assertTrue(move.mrp_move_up_ids) + if move.mrp_move_up_ids.mrp_product_id.product_id == self.fp_1: + # Demand coming from FP-1 + self.assertEqual(move.mrp_move_up_ids.mrp_action, 'mo') + self.assertEqual(move.mrp_qty, -200.0) + elif move.mrp_move_up_ids.mrp_product_id.product_id == self.sf_1: + # Demand coming from FP-2 -> SF-1 + self.assertEqual(move.mrp_move_up_ids.mrp_action, 'mo') + if move.mrp_date == self.date_5: + self.assertEqual(move.mrp_qty, -90.0) + elif move.mrp_date == self.date_8: + self.assertEqual(move.mrp_qty, -72.0) + # Check actions: + moves = self.mrp_move_obj.search([ + ('product_id', '=', self.pp_1.id), + ('mrp_action', '!=', 'none'), + ]) + self.assertEqual(len(moves), 3) + for move in moves: + self.assertEqual(move.mrp_action, 'po') + self.assertEqual(move.mrp_type, 's') + # Check PP-2 PO being accounted: + po_move = self.mrp_move_obj.search([ + ('product_id', '=', self.pp_2.id), + ('mrp_action', '=', 'none'), + ('mrp_type', '=', 's'), + ]) + self.assertEqual(len(po_move), 1) + self.assertEqual(po_move.purchase_order_id, self.po) + self.assertEqual(po_move.purchase_line_id, self.po.order_line) + + def test_04_multi_level_mrp(self): """Tests MRP inventories created.""" # FP-1 - fp_1_inventory_lines = self.mrp_inventory_model.search( + fp_1_inventory_lines = self.mrp_inventory_obj.search( [('mrp_product_id.product_id', '=', self.fp_1.id)]) self.assertEqual(len(fp_1_inventory_lines), 1) - date_7 = fields.Date.to_string(datetime.today() + timedelta(days=7)) - self.assertEqual(fp_1_inventory_lines.date, date_7) + self.assertEqual(fp_1_inventory_lines.date, self.date_7) self.assertEqual(fp_1_inventory_lines.demand_qty, 100.0) self.assertEqual(fp_1_inventory_lines.to_procure, 100.0) # FP-2 - fp_2_inventory_lines = self.mrp_inventory_model.search( - [('mrp_product_id.product_id', '=', self.fp_2.id)]) - self.assertEqual(len(fp_2_inventory_lines), 1) - self.assertEqual(fp_2_inventory_lines.date, date_7) - self.assertEqual(fp_2_inventory_lines.demand_qty, 15.0) - self.assertEqual(fp_2_inventory_lines.to_procure, 15.0) + fp_2_line_1 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.fp_2.id), + ('date', '=', self.date_7)]) + self.assertEqual(len(fp_2_line_1), 1) + self.assertEqual(fp_2_line_1.demand_qty, 15.0) + self.assertEqual(fp_2_line_1.to_procure, 15.0) + # TODO: ask odoo to fix it... should be date10 + fp_2_line_2 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.fp_2.id), + ('date', '=', self.date_9)]) + self.assertEqual(len(fp_2_line_2), 1) + self.assertEqual(fp_2_line_2.demand_qty, 0.0) + self.assertEqual(fp_2_line_2.to_procure, 0.0) + self.assertEqual(fp_2_line_2.supply_qty, 12.0) # SF-1 - sf_1_inventory_lines = self.mrp_inventory_model.search( - [('mrp_product_id.product_id', '=', self.sf_1.id)]) - self.assertEqual(len(sf_1_inventory_lines), 1) - date_6 = fields.Date.to_string(datetime.today() + timedelta(days=6)) - self.assertEqual(sf_1_inventory_lines.date, date_6) - self.assertEqual(sf_1_inventory_lines.demand_qty, 30.0) - self.assertEqual(sf_1_inventory_lines.to_procure, 30.0) + sf_1_line_1 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.sf_1.id), + ('date', '=', self.date_6)]) + self.assertEqual(len(sf_1_line_1), 1) + self.assertEqual(sf_1_line_1.demand_qty, 30.0) + self.assertEqual(sf_1_line_1.to_procure, 30.0) + sf_1_line_2 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.sf_1.id), + ('date', '=', self.date_9)]) + self.assertEqual(len(sf_1_line_2), 1) + self.assertEqual(sf_1_line_2.demand_qty, 24.0) + self.assertEqual(sf_1_line_2.to_procure, 24.0) # SF-2 - sf_2_inventory_lines = self.mrp_inventory_model.search( - [('mrp_product_id.product_id', '=', self.sf_2.id)]) - self.assertEqual(len(sf_2_inventory_lines), 1) - self.assertEqual(sf_2_inventory_lines.date, date_6) - self.assertEqual(sf_2_inventory_lines.demand_qty, 45.0) - self.assertEqual(sf_2_inventory_lines.to_procure, 30.0) + sf_2_line_1 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.sf_2.id), + ('date', '=', self.date_6)]) + self.assertEqual(len(sf_2_line_1), 1) + self.assertEqual(sf_2_line_1.demand_qty, 45.0) + self.assertEqual(sf_2_line_1.to_procure, 30.0) + sf_2_line_2 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.sf_2.id), + ('date', '=', self.date_9)]) + self.assertEqual(len(sf_2_line_2), 1) + self.assertEqual(sf_2_line_2.demand_qty, 36.0) + self.assertEqual(sf_2_line_2.to_procure, 36.0) # PP-1 - pp_1_inventory_lines = self.mrp_inventory_model.search( - [('mrp_product_id.product_id', '=', self.pp_1.id)]) - self.assertEqual(len(pp_1_inventory_lines), 1) - date_5 = fields.Date.to_string(datetime.today() + timedelta(days=5)) - self.assertEqual(pp_1_inventory_lines.date, date_5) - self.assertEqual(pp_1_inventory_lines.demand_qty, 290.0) - self.assertEqual(pp_1_inventory_lines.to_procure, 280.0) + pp_1_line_1 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.pp_1.id), + ('date', '=', self.date_5)]) + self.assertEqual(len(pp_1_line_1), 1) + self.assertEqual(pp_1_line_1.demand_qty, 290.0) + self.assertEqual(pp_1_line_1.to_procure, 280.0) + pp_1_line_2 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.pp_1.id), + ('date', '=', self.date_8)]) + self.assertEqual(len(pp_1_line_2), 1) + self.assertEqual(pp_1_line_2.demand_qty, 72.0) + self.assertEqual(pp_1_line_2.to_procure, 72.0) # PP-2 - pp_2_line_1 = self.mrp_inventory_model.search([ + pp_2_line_1 = self.mrp_inventory_obj.search([ ('mrp_product_id.product_id', '=', self.pp_2.id), - ('date', '=', date_5)]) + ('date', '=', self.date_3)]) self.assertEqual(len(pp_2_line_1), 1) - self.assertEqual(pp_2_line_1.demand_qty, 360.0) - self.assertEqual(pp_2_line_1.to_procure, 360.0) - date_3 = fields.Date.to_string(datetime.today() + timedelta(days=3)) - pp_2_line_2 = self.mrp_inventory_model.search([ + self.assertEqual(pp_2_line_1.demand_qty, 90.0) + # 90.0 demand - 20.0 on hand - 5.0 on PO = 65.0 + self.assertEqual(pp_2_line_1.to_procure, 65.0) + pp_2_line_2 = self.mrp_inventory_obj.search([ ('mrp_product_id.product_id', '=', self.pp_2.id), - ('date', '=', date_3)]) + ('date', '=', self.date_5)]) self.assertEqual(len(pp_2_line_2), 1) - self.assertEqual(pp_2_line_2.demand_qty, 90.0) - self.assertEqual(pp_2_line_2.to_procure, 70.0) + self.assertEqual(pp_2_line_2.demand_qty, 360.0) + self.assertEqual(pp_2_line_2.to_procure, 360.0) + pp_2_line_3 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.pp_2.id), + ('date', '=', self.date_6)]) + self.assertEqual(len(pp_2_line_3), 1) + self.assertEqual(pp_2_line_3.demand_qty, 108.0) + self.assertEqual(pp_2_line_3.to_procure, 108.0) + pp_2_line_4 = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.pp_2.id), + ('date', '=', self.date_8)]) + self.assertEqual(len(pp_2_line_4), 1) + self.assertEqual(pp_2_line_4.demand_qty, 48.0) + self.assertEqual(pp_2_line_4.to_procure, 48.0) - # TODO: test procure wizard: dates... + def test_05_actions_count(self): + """Test actions counters.""" + pass + + def test_06_procure_mo(self): + """Test procurement wizard with MOs.""" + mos = self.mo_obj.search([ + ('product_id', '=', self.fp_1.id)]) + self.assertFalse(mos) + mrp_inv = self.mrp_inventory_obj.search([ + ('mrp_product_id.product_id', '=', self.fp_1.id)]) + self.mrp_inventory_procure_wiz.with_context({ + 'active_model': 'mrp.inventory', + 'active_ids': mrp_inv.ids, + 'active_id': mrp_inv.id, + }).create({}).make_procurement() + mos = self.mo_obj.search([ + ('product_id', '=', self.fp_1.id)]) + self.assertTrue(mos) + self.assertEqual(mos.product_qty, 100.0) + datetime_5 = fields.Datetime.to_string( + date.today() + timedelta(days=5)) + self.assertEqual(mos.date_planned_start, datetime_5) + + # TODO: test procure wizard: pos, multiple... + # TODO: test multiple destination IDS:... \ No newline at end of file diff --git a/multi_level_mrp/views/mrp_product_view.xml b/multi_level_mrp/views/mrp_product_view.xml index 647d5f741..ade518fa8 100644 --- a/multi_level_mrp/views/mrp_product_view.xml +++ b/multi_level_mrp/views/mrp_product_view.xml @@ -138,7 +138,7 @@ - + diff --git a/multi_level_mrp/wizards/multi_level_mrp.py b/multi_level_mrp/wizards/multi_level_mrp.py index 2b961a9df..5668b6a7b 100644 --- a/multi_level_mrp/wizards/multi_level_mrp.py +++ b/multi_level_mrp/wizards/multi_level_mrp.py @@ -4,6 +4,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models, exceptions, _ +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT from datetime import date, datetime, timedelta import logging @@ -13,25 +14,13 @@ logger = logging.getLogger(__name__) class MultiLevelMrp(models.TransientModel): _name = 'multi.level.mrp' - # TODO: fix supply_method calculation. # TODO: dates are not being correctly computed for supply... @api.model def _prepare_mrp_product_data(self, product, mrp_area): - main_supplier_id = False - sequence = 9999 - # TODO: All this should not be really needed, as at the time - # of procurement you will figure out these details. - for supplier in product.product_tmpl_id.seller_ids: - if supplier.sequence < sequence: - sequence = supplier.sequence - main_supplier_id = supplier.name.id - supply_method = 'produce' - for route in product.product_tmpl_id.route_ids: - if route.name == 'Buy': - supply_method = 'buy' qty_available = 0.0 product_obj = self.env['product.product'] + # TODO: move mrp_qty_available computation, maybe unreserved?? location_ids = self.env['stock.location'].search( [('id', 'child_of', mrp_area.location_id.id)]) for location in location_ids: @@ -44,11 +33,7 @@ class MultiLevelMrp(models.TransientModel): 'product_id': product.id, 'mrp_qty_available': product.qty_available, 'mrp_llc': product.llc, - 'nbr_mrp_actions': 0, - 'nbr_mrp_actions_4w': 0, 'name': product.name, - 'supply_method': supply_method, - 'main_supplier_id': main_supplier_id, } @api.model @@ -86,146 +71,142 @@ class MultiLevelMrp(models.TransientModel): } @api.model - def _prepare_mrp_move_data_from_stock_move(self, mrp_product, move): + def _prepare_mrp_move_data_from_stock_move( + self, mrp_product, move, direction='in'): # TODO: Clean up to reduce dependencies - if (move.location_id.usage == 'internal' and + if not((move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal') \ or (move.location_id.usage != 'internal' and - move.location_dest_id.usage == 'internal'): - if move.location_id.usage == 'internal': - mrp_type = 'd' - productqty = -move.product_qty - else: - mrp_type = 's' - productqty = move.product_qty - po = None - po_line = None - so = None - so_line = None - mo = None - origin = None - order_number = None - parent_product_id = None - if move.purchase_line_id: - order_number = move.purchase_line_id.order_id.name - origin = 'po' - po = move.purchase_line_id.order_id.id - po_line = move.purchase_line_id.id - if move.production_id: - order_number = move.production_id.name - origin = 'mo' - mo = move.production_id.id - else: - # TODO: move.move_dest_id -> move.move_dest_ids - if move.move_dest_ids: - move_dest_id = move.move_dest_ids[0] - if move.move_dest_id.production_id: - order_number = \ - move_dest_id.production_id.name + move.location_dest_id.usage == 'internal')): + # TODO: not sure about this... + return {} + if direction == 'out': + mrp_type = 'd' + product_qty = -move.product_qty + else: + mrp_type = 's' + product_qty = move.product_qty + po = po_line = so = so_line = None + mo = origin = order_number = parent_product_id = None + if move.purchase_line_id: + order_number = move.purchase_line_id.order_id.name + origin = 'po' + po = move.purchase_line_id.order_id.id + po_line = move.purchase_line_id.id + if move.production_id: + order_number = move.production_id.name + origin = 'mo' + mo = move.production_id.id + else: + # TODO: move.move_dest_id -> move.move_dest_ids. DONE, review + if move.move_dest_ids: + # move_dest_id = move.move_dest_ids[:1] + for move_dest_id in move.move_dest_ids: + if move_dest_id.production_id: + order_number = move_dest_id.production_id.name origin = 'mo' mo = move_dest_id.production_id.id if move_dest_id.production_id.product_id: parent_product_id = \ move_dest_id.production_id.product_id.id else: - parent_product_id = \ - move_dest_id.product_id.id - if order_number is None: - order_number = move.name - mrp_date = date.today() - if datetime.date(datetime.strptime( - move.date_expected, '%Y-%m-%d %H:%M:%S')) > date.today(): - mrp_date = datetime.date(datetime.strptime( - move.date_expected, '%Y-%m-%d %H:%M:%S')) - return { - 'mrp_area_id': mrp_product.mrp_area_id.id, - 'product_id': move.product_id.id, - 'mrp_product_id': mrp_product.id, - 'production_id': mo, - 'purchase_order_id': po, - 'purchase_line_id': po_line, - 'sale_order_id': so, - 'sale_line_id': so_line, - 'stock_move_id': move.id, - 'mrp_qty': productqty, - 'current_qty': productqty, - 'mrp_date': mrp_date, - 'current_date': move.date_expected, - 'mrp_action': 'none', - 'mrp_type': mrp_type, - 'mrp_processed': False, - 'mrp_origin': origin, - 'mrp_order_number': order_number, - 'parent_product_id': parent_product_id, - 'running_availability': 0.00, - 'name': order_number, - 'state': move.state, - } - return {} - - @api.model - def _prepare_mrp_move_data_supply(self, product, qty, mrp_date_supply, - mrp_action_date, mrp_action, name): + parent_product_id = move_dest_id.product_id.id + if order_number is None: + order_number = move.name + mrp_date = date.today() + if datetime.date(datetime.strptime( + move.date_expected, + DEFAULT_SERVER_DATETIME_FORMAT)) > date.today(): + mrp_date = datetime.date(datetime.strptime( + move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)) return { - 'mrp_area_id': product.mrp_area_id.id, - 'product_id': product.product_id.id, - 'mrp_product_id': product.id, - 'production_id': None, - 'purchase_order_id': None, - 'purchase_line_id': None, - 'sale_order_id': None, - 'sale_line_id': None, - 'stock_move_id': None, - 'mrp_qty': qty, - 'current_qty': None, - 'mrp_date': mrp_date_supply, - 'mrp_action_date': mrp_action_date, - 'current_date': None, - 'mrp_action': mrp_action, - 'mrp_type': 's', - 'mrp_processed': False, - 'mrp_origin': None, - 'mrp_order_number': None, - 'parent_product_id': None, - 'name': 'Supply: ' + name, - } + 'mrp_area_id': mrp_product.mrp_area_id.id, + 'product_id': move.product_id.id, + 'mrp_product_id': mrp_product.id, + 'production_id': mo, + 'purchase_order_id': po, + 'purchase_line_id': po_line, + 'sale_order_id': so, + 'sale_line_id': so_line, + 'stock_move_id': move.id, + 'mrp_qty': product_qty, + 'current_qty': product_qty, + 'mrp_date': mrp_date, + 'current_date': move.date_expected, + 'mrp_action': 'none', + 'mrp_type': mrp_type, + 'mrp_processed': False, + 'mrp_origin': origin, + 'mrp_order_number': order_number, + 'parent_product_id': parent_product_id, + 'running_availability': 0.00, + 'name': order_number, + 'state': move.state, + } @api.model - def _prepare_mrp_move_data_bom_explosion(self, product, bomline, qty, - mrp_date_demand_2, bom, name): - mrp_products = self.env['mrp.product'].search( - [('mrp_area_id', '=', product.mrp_area_id.id), - ('product_id', '=', bomline.product_id.id)], limit=1) - if not mrp_products: - raise exceptions.Warning( - _("No MRP product found")) + def _prepare_mrp_move_data_supply( + self, mrp_product, qty, mrp_date_supply, mrp_action_date, + mrp_action, name): + return { + 'mrp_area_id': mrp_product.mrp_area_id.id, + 'product_id': mrp_product.product_id.id, + 'mrp_product_id': mrp_product.id, + 'production_id': None, + 'purchase_order_id': None, + 'purchase_line_id': None, + 'sale_order_id': None, + 'sale_line_id': None, + 'stock_move_id': None, + 'mrp_qty': qty, + 'current_qty': None, + 'mrp_date': mrp_date_supply, + 'mrp_action_date': mrp_action_date, + 'current_date': None, + 'mrp_action': mrp_action, + 'mrp_type': 's', + 'mrp_processed': False, + 'mrp_origin': None, + 'mrp_order_number': None, + 'parent_product_id': None, + 'name': 'Supply: ' + name, + } - return { - 'mrp_area_id': product.mrp_area_id.id, - 'product_id': bomline.product_id.id, - 'mrp_product_id': mrp_products[0].id, - 'production_id': None, - 'purchase_order_id': None, - 'purchase_line_id': None, - 'sale_order_id': None, - 'sale_line_id': None, - 'stock_move_id': None, - 'mrp_qty': -(qty * bomline.product_qty), - 'current_qty': None, - 'mrp_date': mrp_date_demand_2, - 'current_date': None, - 'mrp_action': 'none', - 'mrp_type': 'd', - 'mrp_processed': False, - 'mrp_origin': 'mrp', - 'mrp_order_number': None, - 'parent_product_id': bom.product_id.id, - 'name': - ('Demand Bom Explosion: ' + name).replace( - 'Demand Bom Explosion: Demand Bom ' - 'Explosion: ', - 'Demand Bom Explosion: '), - } + @api.model + def _prepare_mrp_move_data_bom_explosion( + self, product, bomline, qty, mrp_date_demand_2, bom, name): + mrp_product = self._get_mrp_product_from_product_and_area( + bomline.product_id, product.mrp_area_id) + if not mrp_product: + raise exceptions.Warning( + _("No MRP product found")) + + return { + 'mrp_area_id': product.mrp_area_id.id, + 'product_id': bomline.product_id.id, + 'mrp_product_id': mrp_product.id, + 'production_id': None, + 'purchase_order_id': None, + 'purchase_line_id': None, + 'sale_order_id': None, + 'sale_line_id': None, + 'stock_move_id': None, + 'mrp_qty': -(qty * bomline.product_qty), # TODO: review with UoM + 'current_qty': None, + 'mrp_date': mrp_date_demand_2, + 'current_date': None, + 'mrp_action': 'none', + 'mrp_type': 'd', + 'mrp_processed': False, + 'mrp_origin': 'mrp', + 'mrp_order_number': None, + 'parent_product_id': bom.product_id.id, + 'name': + ('Demand Bom Explosion: ' + name).replace( + 'Demand Bom Explosion: Demand Bom ' + 'Explosion: ', + 'Demand Bom Explosion: '), + } @api.model def create_move(self, mrp_product_id, mrp_date, mrp_qty, name): @@ -233,90 +214,73 @@ class MultiLevelMrp(models.TransientModel): values = {} if not isinstance(mrp_date, date): - mrp_date = datetime.date(datetime.strptime(mrp_date, '%Y-%m-%d')) + mrp_date = fields.Date.from_string(mrp_date) + + if mrp_product_id.supply_method == 'buy': + # if mrp_product_id.purchase_requisition: + # mrp_action = 'pr' + # else: + mrp_action = 'po' + else: + # TODO: consider 'none'... + mrp_action = 'mo' + + if mrp_date < date.today(): + mrp_date_supply = date.today() + else: + mrp_date_supply = mrp_date + + mrp_action_date = mrp_date - timedelta( + days=mrp_product_id.mrp_lead_time) qty_ordered = 0.00 - products = self.env['mrp.product'].search([('id', '=', - mrp_product_id)]) - for product in products: - if product.supply_method == 'buy': - # if product.purchase_requisition: - # mrp_action = 'pr' - # else: - mrp_action = 'po' - else: - mrp_action = 'mo' + qty_to_order = mrp_qty + while qty_ordered < mrp_qty: + qty = mrp_product_id._adjust_qty_to_order(qty_to_order) + qty_to_order -= qty + move_data = self._prepare_mrp_move_data_supply( + mrp_product_id, qty, mrp_date_supply, mrp_action_date, + mrp_action, name) + mrpmove_id = self.env['mrp.move'].create(move_data) + qty_ordered = qty_ordered + qty - if mrp_date < date.today(): - mrp_date_supply = date.today() - else: - mrp_date_supply = mrp_date - - mrp_action_date = mrp_date-timedelta(days=product.mrp_lead_time) - - qty_ordered = 0.00 - qty_to_order = mrp_qty - while qty_ordered < mrp_qty: - qty = 0.00 - if product.mrp_maximum_order_qty == 0.00 and \ - product.mrp_minimum_order_qty == 0.00: - qty = qty_to_order - else: - if qty_to_order < product.mrp_minimum_order_qty: - qty = product.mrp_minimum_order_qty - else: - if product.mrp_maximum_order_qty and qty_to_order > \ - product.mrp_maximum_order_qty: - qty = product.mrp_maximum_order_qty - else: - qty = qty_to_order - qty_to_order -= qty - - move_data = self._prepare_mrp_move_data_supply(product, qty, - mrp_date_supply, - mrp_action_date, - mrp_action, - name) - mrpmove_id = self.env['mrp.move'].create(move_data) - qty_ordered = qty_ordered + qty - - if mrp_action == 'mo': - mrp_date_demand = mrp_date-timedelta(days=product.mrp_lead_time) - if mrp_date_demand < date.today(): - mrp_date_demand = date.today() - if not product.product_id.bom_ids: + if mrp_action == 'mo': + mrp_date_demand = mrp_date - timedelta( + days=mrp_product_id.mrp_lead_time) + if mrp_date_demand < date.today(): + mrp_date_demand = date.today() + if not mrp_product_id.product_id.bom_ids: + continue + bomcount = 0 + for bom in mrp_product_id.product_id.bom_ids: + if not bom.active or not bom.bom_line_ids: continue - bomcount = 0 - for bom in product.product_id.bom_ids: - if not bom.active or not bom.bom_line_ids: + bomcount += 1 + if bomcount != 1: + continue + for bomline in bom.bom_line_ids: + if bomline.product_qty <= 0.00: continue - bomcount += 1 - if bomcount != 1: + if self._exclude_from_mrp( + mrp_product_id.mrp_area_id, bomline.product_id): + # Stop explosion. continue - for bomline in bom.bom_line_ids: - if bomline.product_qty <= 0.00: - continue - if self._exclude_from_mrp( - product.mrp_area_id, bomline.product_id): - # Stop explosion. - continue - mrp_date_demand_2 = mrp_date_demand-timedelta( - days=(product.mrp_transit_delay+product. - mrp_inspection_delay)) - move_data = \ - self._prepare_mrp_move_data_bom_explosion( - product, bomline, qty, - mrp_date_demand_2, - bom, name) - mrpmove_id2 = self.env['mrp.move'].create( - move_data) - sql_stat = "INSERT INTO mrp_move_rel (" \ - "move_up_id, " \ - "move_down_id) values (%d, %d)" % \ - (mrpmove_id, mrpmove_id2, ) - self.env.cr.execute(sql_stat) + mrp_date_demand_2 = mrp_date_demand - timedelta( # TODO: review this... + days=(mrp_product_id.mrp_transit_delay + + mrp_product_id.mrp_inspection_delay)) + move_data = \ + self._prepare_mrp_move_data_bom_explosion( + mrp_product_id, bomline, qty, + mrp_date_demand_2, + bom, name) + mrpmove_id2 = self.env['mrp.move'].create(move_data) + sql_stat = "INSERT INTO mrp_move_rel (" \ + "move_up_id, " \ + "move_down_id) values (%d, %d)" % \ + (mrpmove_id, mrpmove_id2, ) + self.env.cr.execute(sql_stat) values['qty_ordered'] = qty_ordered - log_msg = '%s' % qty_ordered + log_msg = '%s' % qty_ordered logger.info(log_msg) return values @@ -452,30 +416,50 @@ class MultiLevelMrp(models.TransientModel): self.env['mrp.move'].create(mrp_move_data) return True + # TODO: move this methods to mrp_product?? to be able to show moves with an action + @api.model + def _in_stock_moves_domain(self, mrp_product): + locations = self.env['stock.location'].search( + [('id', 'child_of', mrp_product.mrp_area_id.location_id.id)]) + return [ + ('product_id', '=', mrp_product.product_id.id), + ('state', 'not in', ['done', 'cancel']), + ('product_qty', '>', 0.00), + ('location_id', 'not in', locations.ids), + ('location_dest_id', 'in', locations.ids), + ] + + @api.model + def _out_stock_moves_domain(self, mrp_product): + locations = self.env['stock.location'].search( + [('id', 'child_of', mrp_product.mrp_area_id.location_id.id)]) + return [ + ('product_id', '=', mrp_product.product_id.id), + ('state', 'not in', ['done', 'cancel']), + ('product_qty', '>', 0.00), + ('location_id', 'in', locations.ids), + ('location_dest_id', 'not in', locations.ids), + ] + @api.model def _init_mrp_move_from_stock_move(self, mrp_product): # TODO: Should we exclude the quantity done from the moves? move_obj = self.env['stock.move'] mrp_move_obj = self.env['mrp.move'] - location_ids = self.env['stock.location'].search( - [('id', 'child_of', mrp_product.mrp_area_id.location_id.id)]) - in_moves = move_obj.search( - [('product_id', '=', mrp_product.product_id.id), - ('state', '!=', 'done'), - ('state', '!=', 'cancel'), - ('product_qty', '>', 0.00), - ('location_id', 'in', location_ids.ids)]) - out_moves = move_obj.search( - [('product_id', '=', mrp_product.product_id.id), - ('state', '!=', 'done'), - ('state', '!=', 'cancel'), - ('product_qty', '>', 0.00), - ('location_dest_id', 'in', location_ids.ids)]) - moves = in_moves + out_moves - for move in moves: - move_data = self._prepare_mrp_move_data_from_stock_move( - mrp_product, move) - mrp_move_obj.create(move_data) + in_domain = self._in_stock_moves_domain(mrp_product) + in_moves = move_obj.search(in_domain) + out_domain = self._out_stock_moves_domain(mrp_product) + out_moves = move_obj.search(out_domain) + if in_moves: + for move in in_moves: + move_data = self._prepare_mrp_move_data_from_stock_move( + mrp_product, move, direction='in') + mrp_move_obj.create(move_data) + if out_moves: + for move in out_moves: + move_data = self._prepare_mrp_move_data_from_stock_move( + mrp_product, move, direction='out') + mrp_move_obj.create(move_data) return True # TODO: extension to purchase requisition in other module? @@ -575,16 +559,16 @@ class MultiLevelMrp(models.TransientModel): picking_type_ids = [ptype.id for ptype in picking_types] orders = self.env['purchase.order'].search( [('picking_type_id', 'in', picking_type_ids), - ('state', 'in', ['draft', 'confirmed'])]) + ('state', 'in', ['draft', 'sent', 'to approve'])]) po_lines = self.env['purchase.order.line'].search( [('order_id', 'in', orders.ids), ('product_qty', '>', 0.0), ('product_id', '=', mrp_product.product_id.id)]) - for poline in po_lines: + for line in po_lines: mrp_move_data = \ self._prepare_mrp_move_data_from_purchase_order( - poline, mrp_product) + line, mrp_product) self.env['mrp.move'].create(mrp_move_data) @api.model @@ -620,14 +604,21 @@ class MultiLevelMrp(models.TransientModel): } @api.model - def _prepare_mrp_move_data_from_mrp_production_bom(self, mo, bomline, - mrp_date_demand, - mrp_product): + def _get_mrp_product_from_product_and_area(self, product, mrp_area): + return self.env['mrp.product'].search([ + ('product_id', '=', product.id), + ('mrp_area_id', '=', mrp_area.id), + ], limit=1) + + @api.model + def _prepare_mrp_move_data_from_mrp_production_bom( + self, mo, bomline, mrp_date_demand, mrp_product): + line_mrp_product = self._get_mrp_product_from_product_and_area( + bomline.product_id, mrp_product.mrp_area_id) return { 'mrp_area_id': mrp_product.mrp_area_id.id, 'product_id': bomline.product_id.id, - 'mrp_product_id': - bomline.product_id.mrp_product_id.id, + 'mrp_product_id': line_mrp_product.id, 'production_id': mo.id, 'purchase_order_id': None, 'purchase_line_id': None, @@ -649,23 +640,28 @@ class MultiLevelMrp(models.TransientModel): @api.model def _init_mrp_move_from_mrp_production_bom(self, mo, mrp_product): - mrp_date = date.today() - mrp_date_demand = mrp_date-timedelta( - days=mrp_product.product_id.mrp_lead_time) - if mrp_date_demand < date.today(): - mrp_date_demand = date.today() + # TODO: iniciate this from moves with MOs and for other moves from bom lines?? + # TODO: fix this... + # mrp_date = date.today() + # mrp_date_demand = mrp_date - timedelta( + # days=mrp_product.product_id.produce_delay) + # if mrp_date_demand < date.today(): + # mrp_date_demand = date.today() + mrp_date_demand = date.today() if mo.bom_id and mo.bom_id.bom_line_ids: for bomline in mo.bom_id.bom_line_ids: if bomline.product_qty <= 0.00: continue - if (bomline.date_start and datetime.date( - datetime.strptime(bomline.date_start, '%Y-%m-%d')) >= - mrp_date_demand): - continue - if (bomline.date_stop and datetime.date( - datetime.strptime(bomline.date_stop, '%Y-%m-%d')) <= - mrp_date_demand): - continue + # TODO: ['mrp.bom.line'].date_start does not exist in v11. Remove: + # if (bomline.date_start and datetime.date( + # datetime.strptime(bomline.date_start, '%Y-%m-%d')) >= + # mrp_date_demand): + # continue + # if (bomline.date_stop and datetime.date( + # datetime.strptime(bomline.date_stop, '%Y-%m-%d')) <= + # mrp_date_demand): + # continue + # TODO: add conditions to do this: ddmrp, not already existing MOs (MTO)... mrp_move_data = \ self._prepare_mrp_move_data_from_mrp_production_bom( mo, bomline, mrp_date_demand, mrp_product) @@ -675,15 +671,18 @@ class MultiLevelMrp(models.TransientModel): def _init_mrp_move_from_mrp_production(self, mrp_product): location_ids = self.env['stock.location'].search( [('id', 'child_of', mrp_product.mrp_area_id.location_id.id)]) - production_orders = self.env['mrp.production'].search( - [('location_dest_id', 'in', location_ids.ids), - ('product_qty', '>', 0.0), - ('state', '=', 'draft')]) + # TODO: there is no 'draft' state anymore. there will always be stock.moves + production_orders = self.env['mrp.production'].search([ + ('product_id', '=', mrp_product.product_id.id), + ('location_dest_id', 'in', location_ids.ids), + ('product_qty', '>', 0.0), + ('state', 'in', ['confirmed', 'planned']), + ]) # TODO: 'progress' as well? for mo in production_orders: - mrp_move_data = \ - self._prepare_mrp_move_data_from_mrp_production( - mo, mrp_product) - self.env['mrp.move'].create(mrp_move_data) + # mrp_move_data = \ + # self._prepare_mrp_move_data_from_mrp_production( + # mo, mrp_product) + # self.env['mrp.move'].create(mrp_move_data) self._init_mrp_move_from_mrp_production_bom(mo, mrp_product) @api.model @@ -693,7 +692,8 @@ class MultiLevelMrp(models.TransientModel): # TODO: extension to purchase requisition in other module? # self._init_mrp_move_from_purchase_requisition(mrp_product) self._init_mrp_move_from_purchase_order(mrp_product) - self._init_mrp_move_from_mrp_production(mrp_product) + # TODO: not needed I think... check case when MO are partially done and posted... + # self._init_mrp_move_from_mrp_production(mrp_product) @api.model def _exclude_from_mrp(self, mrp_area, product): @@ -716,6 +716,7 @@ class MultiLevelMrp(models.TransientModel): init_counter, product.default_code) logger.info(log_msg) mrp_product = self._init_mrp_product(product, mrp_area) + # for mrp_product in self.env['mrp.product'].search([]): self._init_mrp_move(mrp_product) # self.env.cr.commit() logger.info('END MRP INITIALISATION') @@ -803,46 +804,45 @@ class MultiLevelMrp(models.TransientModel): for mrp_product in mrp_products: nbr_create = 0 - onhand = mrp_product.mrp_qty_available + onhand = mrp_product.mrp_qty_available # TODO: qty unreserved? if mrp_product.mrp_nbr_days == 0: # todo: review ordering by date for move in mrp_product.mrp_move_ids: if move.mrp_action == 'none': if (onhand + move.mrp_qty) < \ mrp_product.mrp_minimum_stock: - name = move.name qtytoorder = \ mrp_product.mrp_minimum_stock - \ onhand - move.mrp_qty cm = self.create_move( - mrp_product_id=mrp_product.id, + mrp_product_id=mrp_product, mrp_date=move.mrp_date, - mrp_qty=qtytoorder, name=name) + mrp_qty=qtytoorder, name=move.name) qty_ordered = cm['qty_ordered'] onhand += move.mrp_qty + qty_ordered nbr_create += 1 else: onhand += move.mrp_qty else: + # TODO: review this nbr_create = self._init_mrp_move_grouped_demand( nbr_create, mrp_product) if onhand < mrp_product.mrp_minimum_stock and \ nbr_create == 0: - name = 'Minimum Stock' qtytoorder = mrp_product.mrp_minimum_stock - onhand - cm = self.create_move(mrp_product_id=mrp_product.id, - mrp_date=date.today(), - mrp_qty=qtytoorder, name=name) + cm = self.create_move( + mrp_product_id=mrp_product, + mrp_date=date.today(), + mrp_qty=qtytoorder, + name='Minimum Stock') qty_ordered = cm['qty_ordered'] onhand += qty_ordered counter += 1 # self.env.cr.commit() - log_msg = 'MRP CALCULATION LLC %s FINISHED - NBR PRODUCTS: %s' %( + log_msg = 'MRP CALCULATION LLC %s FINISHED - NBR PRODUCTS: %s' % ( llc - 1, counter) logger.info(log_msg) - if llc < 0: - counter = 999999 # self.env.cr.commit() logger.info('END MRP CALCULATION')