From cd26ec8c68793fa1b3700f564672f03f1207d704 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Tue, 17 Dec 2019 15:09:48 +0100 Subject: [PATCH] [IMP] mrp_multi_level: black, isort --- mrp_multi_level/__manifest__.py | 74 +-- .../migrations/11.0.2.0.0/post-migration.py | 62 +- mrp_multi_level/models/mrp_area.py | 23 +- mrp_multi_level/models/mrp_inventory.py | 80 +-- mrp_multi_level/models/mrp_move.py | 83 ++- mrp_multi_level/models/mrp_planned_order.py | 26 +- mrp_multi_level/models/product_mrp_area.py | 163 ++--- mrp_multi_level/models/product_product.py | 49 +- mrp_multi_level/models/product_template.py | 40 +- mrp_multi_level/models/stock_location.py | 7 +- mrp_multi_level/tests/common.py | 629 ++++++++++-------- mrp_multi_level/tests/test_mrp_multi_level.py | 279 ++++---- .../wizards/mrp_inventory_procure.py | 131 ++-- mrp_multi_level/wizards/mrp_multi_level.py | 595 +++++++++-------- 14 files changed, 1193 insertions(+), 1048 deletions(-) diff --git a/mrp_multi_level/__manifest__.py b/mrp_multi_level/__manifest__.py index d1f925a93..ebfb8b027 100644 --- a/mrp_multi_level/__manifest__.py +++ b/mrp_multi_level/__manifest__.py @@ -3,46 +3,40 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). { - 'name': 'MRP Multi Level', - 'version': '12.0.1.0.1', - 'development_status': 'Beta', - 'license': 'LGPL-3', - 'author': 'Ucamco, ' - 'Eficent, ' - 'Odoo Community Association (OCA)', - 'maintainers': ['jbeficent', 'lreficent'], - 'summary': 'Adds an MRP Scheduler', - 'website': 'https://github.com/OCA/manufacture', - 'category': 'Manufacturing', - 'depends': [ - 'mrp', - 'purchase_stock', - 'mrp_warehouse_calendar', + "name": "MRP Multi Level", + "version": "12.0.1.0.1", + "development_status": "Beta", + "license": "LGPL-3", + "author": "Ucamco, " "Eficent, " "Odoo Community Association (OCA)", + "maintainers": ["jbeficent", "lreficent"], + "summary": "Adds an MRP Scheduler", + "website": "https://github.com/OCA/manufacture", + "category": "Manufacturing", + "depends": ["mrp", "purchase_stock", "mrp_warehouse_calendar"], + "data": [ + "security/mrp_multi_level_security.xml", + "security/ir.model.access.csv", + "views/mrp_area_views.xml", + "views/product_product_views.xml", + "views/product_template_views.xml", + "views/product_mrp_area_views.xml", + "views/stock_location_views.xml", + "wizards/mrp_inventory_procure_views.xml", + "views/mrp_inventory_views.xml", + "wizards/mrp_multi_level_views.xml", + "views/mrp_menuitem.xml", + "data/mrp_multi_level_cron.xml", + "data/mrp_area_data.xml", ], - 'data': [ - 'security/mrp_multi_level_security.xml', - 'security/ir.model.access.csv', - 'views/mrp_area_views.xml', - 'views/product_product_views.xml', - 'views/product_template_views.xml', - 'views/product_mrp_area_views.xml', - 'views/stock_location_views.xml', - 'wizards/mrp_inventory_procure_views.xml', - 'views/mrp_inventory_views.xml', - 'wizards/mrp_multi_level_views.xml', - 'views/mrp_menuitem.xml', - 'data/mrp_multi_level_cron.xml', - 'data/mrp_area_data.xml', + "demo": [ + "demo/product_category_demo.xml", + "demo/product_product_demo.xml", + "demo/res_partner_demo.xml", + "demo/product_supplierinfo_demo.xml", + "demo/product_mrp_area_demo.xml", + "demo/mrp_bom_demo.xml", + "demo/initial_on_hand_demo.xml", ], - 'demo': [ - 'demo/product_category_demo.xml', - 'demo/product_product_demo.xml', - 'demo/res_partner_demo.xml', - 'demo/product_supplierinfo_demo.xml', - 'demo/product_mrp_area_demo.xml', - 'demo/mrp_bom_demo.xml', - 'demo/initial_on_hand_demo.xml', - ], - 'installable': True, - 'application': True, + "installable": True, + "application": True, } diff --git a/mrp_multi_level/migrations/11.0.2.0.0/post-migration.py b/mrp_multi_level/migrations/11.0.2.0.0/post-migration.py index afd5d1925..54434b935 100644 --- a/mrp_multi_level/migrations/11.0.2.0.0/post-migration.py +++ b/mrp_multi_level/migrations/11.0.2.0.0/post-migration.py @@ -1,7 +1,8 @@ # Copyright 2019 Eficent Business and IT Consulting Services, S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). import logging -from odoo import api, SUPERUSER_ID + +from odoo import SUPERUSER_ID, api _logger = logging.getLogger(__name__) @@ -10,7 +11,8 @@ __name__ = "Upgrade to 11.0.2.0.0" def _migrate_product_to_product_mrp_area(env): _logger.info("Migrating product parameters to Product MRP Areas") - env.cr.execute(""" + env.cr.execute( + """ SELECT DISTINCT mrp_area.id, pr.id, pr.mrp_applicable, pr.mrp_exclude, pr.mrp_inspection_delay, pr.mrp_maximum_order_qty, pr.mrp_minimum_order_qty, pr.mrp_minimum_stock, pr.mrp_nbr_days, @@ -21,27 +23,41 @@ def _migrate_product_to_product_mrp_area(env): ON pt.id = pr.product_tmpl_id WHERE pr.mrp_exclude = False AND pt.type = 'product' - """) - product_mrp_area_model = env['product.mrp.area'] - for mrp_area_id, product_id, mrp_applicable, mrp_exclude,\ - mrp_inspection_delay, mrp_maximum_order_qty, mrp_minimum_order_qty, \ - mrp_minimum_stock, mrp_nbr_days, mrp_qty_multiple, mrp_transit_delay,\ - mrp_verified, active in env.cr.fetchall(): - product_mrp_area_model.create({ - 'mrp_area_id': mrp_area_id, - 'product_id': product_id, - 'mrp_applicable': mrp_applicable, - 'mrp_exclude': mrp_exclude, - 'mrp_inspection_delay': mrp_inspection_delay, - 'mrp_maximum_order_qty': mrp_maximum_order_qty, - 'mrp_minimum_order_qty': mrp_minimum_order_qty, - 'mrp_minimum_stock': mrp_minimum_stock, - 'mrp_nbr_days': mrp_nbr_days, - 'mrp_qty_multiple': mrp_qty_multiple, - 'mrp_transit_delay': mrp_transit_delay, - 'mrp_verified': mrp_verified, - 'active': active, - }) + """ + ) + product_mrp_area_model = env["product.mrp.area"] + for ( + mrp_area_id, + product_id, + mrp_applicable, + mrp_exclude, + mrp_inspection_delay, + mrp_maximum_order_qty, + mrp_minimum_order_qty, + mrp_minimum_stock, + mrp_nbr_days, + mrp_qty_multiple, + mrp_transit_delay, + mrp_verified, + active, + ) in env.cr.fetchall(): + product_mrp_area_model.create( + { + "mrp_area_id": mrp_area_id, + "product_id": product_id, + "mrp_applicable": mrp_applicable, + "mrp_exclude": mrp_exclude, + "mrp_inspection_delay": mrp_inspection_delay, + "mrp_maximum_order_qty": mrp_maximum_order_qty, + "mrp_minimum_order_qty": mrp_minimum_order_qty, + "mrp_minimum_stock": mrp_minimum_stock, + "mrp_nbr_days": mrp_nbr_days, + "mrp_qty_multiple": mrp_qty_multiple, + "mrp_transit_delay": mrp_transit_delay, + "mrp_verified": mrp_verified, + "active": active, + } + ) def migrate(cr, version): diff --git a/mrp_multi_level/models/mrp_area.py b/mrp_multi_level/models/mrp_area.py index b7c426e2d..7ac94f23c 100644 --- a/mrp_multi_level/models/mrp_area.py +++ b/mrp_multi_level/models/mrp_area.py @@ -8,32 +8,29 @@ from odoo import api, fields, models class MrpArea(models.Model): - _name = 'mrp.area' + _name = "mrp.area" _description = "MRP Area" name = fields.Char(required=True) warehouse_id = fields.Many2one( - comodel_name='stock.warehouse', string='Warehouse', - required=True, + comodel_name="stock.warehouse", string="Warehouse", required=True ) company_id = fields.Many2one( - comodel_name='res.company', - related='warehouse_id.company_id', - store=True, + comodel_name="res.company", related="warehouse_id.company_id", store=True ) location_id = fields.Many2one( - comodel_name='stock.location', string='Location', - required=True, + comodel_name="stock.location", string="Location", required=True ) active = fields.Boolean(default=True) calendar_id = fields.Many2one( - comodel_name='resource.calendar', - string='Working Hours', - related='warehouse_id.calendar_id', + comodel_name="resource.calendar", + string="Working Hours", + related="warehouse_id.calendar_id", ) @api.multi def _get_locations(self): self.ensure_one() - return self.env['stock.location'].search([ - ('id', 'child_of', self.location_id.id)]) + return self.env["stock.location"].search( + [("id", "child_of", self.location_id.id)] + ) diff --git a/mrp_multi_level/models/mrp_inventory.py b/mrp_multi_level/models/mrp_inventory.py index 75ae6b023..a312750db 100644 --- a/mrp_multi_level/models/mrp_inventory.py +++ b/mrp_multi_level/models/mrp_inventory.py @@ -4,68 +4,63 @@ # - Lois Rilo Antelo # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -from odoo import api, fields, models +from datetime import date, timedelta -from datetime import timedelta, date +from odoo import api, fields, models class MrpInventory(models.Model): - _name = 'mrp.inventory' - _order = 'product_mrp_area_id, date' - _description = 'MRP inventory projections' - _rec_name = 'product_mrp_area_id' + _name = "mrp.inventory" + _order = "product_mrp_area_id, date" + _description = "MRP inventory projections" + _rec_name = "product_mrp_area_id" # TODO: name to pass to procurements? # TODO: compute procurement_date to pass to the wizard? not needed for # PO at least. Check for MO and moves mrp_area_id = fields.Many2one( - comodel_name='mrp.area', string='MRP Area', - related='product_mrp_area_id.mrp_area_id', store=True, + comodel_name="mrp.area", + string="MRP Area", + related="product_mrp_area_id.mrp_area_id", + store=True, ) product_mrp_area_id = fields.Many2one( - comodel_name='product.mrp.area', string='Product Parameters', + comodel_name="product.mrp.area", + string="Product Parameters", index=True, required=True, ) company_id = fields.Many2one( - comodel_name='res.company', - related='product_mrp_area_id.mrp_area_id.warehouse_id.company_id', + comodel_name="res.company", + related="product_mrp_area_id.mrp_area_id.warehouse_id.company_id", store=True, ) product_id = fields.Many2one( - comodel_name='product.product', - related='product_mrp_area_id.product_id', + comodel_name="product.product", + related="product_mrp_area_id.product_id", store=True, ) uom_id = fields.Many2one( - comodel_name='uom.uom', string='Product UoM', - compute='_compute_uom_id', + comodel_name="uom.uom", string="Product UoM", compute="_compute_uom_id" ) - date = fields.Date(string='Date') - demand_qty = fields.Float(string='Demand') - supply_qty = fields.Float(string='Supply') - initial_on_hand_qty = fields.Float(string='Starting Inventory') - final_on_hand_qty = fields.Float(string='Forecasted Inventory') + date = fields.Date(string="Date") + demand_qty = fields.Float(string="Demand") + supply_qty = fields.Float(string="Supply") + initial_on_hand_qty = fields.Float(string="Starting Inventory") + final_on_hand_qty = fields.Float(string="Forecasted Inventory") to_procure = fields.Float( - string="To procure", - compute="_compute_to_procure", - store=True, + string="To procure", compute="_compute_to_procure", store=True ) running_availability = fields.Float( string="Planned Availability", - help="Theoretical inventory level if all planned orders" - "were released.", + help="Theoretical inventory level if all planned orders" "were released.", ) order_release_date = fields.Date( - string="Order Release Date", - compute="_compute_order_release_date", - store=True, + string="Order Release Date", compute="_compute_order_release_date", store=True ) planned_order_ids = fields.One2many( - comodel_name="mrp.planned.order", - inverse_name="mrp_inventory_id", - readonly=True, + comodel_name="mrp.planned.order", inverse_name="mrp_inventory_id", readonly=True ) @api.multi @@ -76,14 +71,17 @@ class MrpInventory(models.Model): @api.depends("planned_order_ids", "planned_order_ids.qty_released") def _compute_to_procure(self): for rec in self: - rec.to_procure = sum(rec.planned_order_ids.mapped('mrp_qty')) - \ - sum(rec.planned_order_ids.mapped('qty_released')) + rec.to_procure = sum(rec.planned_order_ids.mapped("mrp_qty")) - sum( + rec.planned_order_ids.mapped("qty_released") + ) @api.multi - @api.depends('product_mrp_area_id', - 'product_mrp_area_id.main_supplierinfo_id', - 'product_mrp_area_id.mrp_lead_time', - 'product_mrp_area_id.mrp_area_id.calendar_id') + @api.depends( + "product_mrp_area_id", + "product_mrp_area_id.main_supplierinfo_id", + "product_mrp_area_id.mrp_lead_time", + "product_mrp_area_id.mrp_area_id.calendar_id", + ) def _compute_order_release_date(self): today = date.today() for rec in self.filtered(lambda r: r.date): @@ -93,10 +91,12 @@ class MrpInventory(models.Model): # dt_date is at the beginning of the day (00:00), # so we can subtract the delay straight forward. order_release_date = rec.mrp_area_id.calendar_id.plan_days( - -delay, dt_date).date() + -delay, dt_date + ).date() else: - order_release_date = fields.Date.from_string( - rec.date) - timedelta(days=delay) + order_release_date = fields.Date.from_string(rec.date) - timedelta( + days=delay + ) if order_release_date < today: order_release_date = today rec.order_release_date = order_release_date diff --git a/mrp_multi_level/models/mrp_move.py b/mrp_multi_level/models/mrp_move.py index 059cd0416..199643fbf 100644 --- a/mrp_multi_level/models/mrp_move.py +++ b/mrp_multi_level/models/mrp_move.py @@ -2,13 +2,13 @@ # © 2016-18 Eficent Business and IT Consulting Services S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -from odoo import models, fields +from odoo import fields, models class MrpMove(models.Model): - _name = 'mrp.move' + _name = "mrp.move" _description = "MRP Move" - _order = 'product_mrp_area_id, mrp_date, mrp_type desc, id' + _order = "product_mrp_area_id, mrp_date, mrp_type desc, id" # TODO: too many indexes... @@ -26,19 +26,19 @@ class MrpMove(models.Model): index=True, ) company_id = fields.Many2one( - comodel_name='res.company', - related='product_mrp_area_id.mrp_area_id.warehouse_id.company_id', + comodel_name="res.company", + related="product_mrp_area_id.mrp_area_id.warehouse_id.company_id", store=True, ) product_id = fields.Many2one( - comodel_name='product.product', - related='product_mrp_area_id.product_id', + comodel_name="product.product", + related="product_mrp_area_id.product_id", store=True, ) - current_date = fields.Date(string='Current Date') - current_qty = fields.Float(string='Current Qty') - mrp_date = fields.Date(string='MRP Date') + current_date = fields.Date(string="Current Date") + current_qty = fields.Float(string="Current Qty") + mrp_date = fields.Date(string="MRP Date") planned_order_up_ids = fields.Many2many( comodel_name="mrp.planned.order", relation="mrp_move_planned_order_rel", @@ -46,49 +46,48 @@ class MrpMove(models.Model): column2="order_id", string="Planned Orders UP", ) - mrp_order_number = fields.Char(string='Order Number') + mrp_order_number = fields.Char(string="Order Number") mrp_origin = fields.Selection( - selection=[('mo', 'Manufacturing Order'), - ('po', 'Purchase Order'), - ('mv', 'Move'), - ('fc', 'Forecast'), - ('mrp', 'MRP')], - string='Origin') - mrp_qty = fields.Float(string='MRP Quantity') - mrp_type = fields.Selection( - selection=[('s', 'Supply'), ('d', 'Demand')], - string='Type', + selection=[ + ("mo", "Manufacturing Order"), + ("po", "Purchase Order"), + ("mv", "Move"), + ("fc", "Forecast"), + ("mrp", "MRP"), + ], + string="Origin", ) - name = fields.Char(string='Description') + mrp_qty = fields.Float(string="MRP Quantity") + mrp_type = fields.Selection( + selection=[("s", "Supply"), ("d", "Demand")], string="Type" + ) + name = fields.Char(string="Description") parent_product_id = fields.Many2one( - comodel_name="product.product", - string="Parent Product", index=True, + comodel_name="product.product", string="Parent Product", index=True ) production_id = fields.Many2one( - comodel_name='mrp.production', - string='Manufacturing Order', index=True, + comodel_name="mrp.production", string="Manufacturing Order", index=True ) purchase_line_id = fields.Many2one( - comodel_name='purchase.order.line', - string='Purchase Order Line', index=True, + comodel_name="purchase.order.line", string="Purchase Order Line", index=True ) purchase_order_id = fields.Many2one( - comodel_name='purchase.order', - string='Purchase Order', index=True, + comodel_name="purchase.order", string="Purchase Order", index=True ) state = fields.Selection( - selection=[('draft', 'Draft'), - ('assigned', 'Assigned'), - ('confirmed', 'Confirmed'), - ('waiting', 'Waiting'), - ('partially_available', 'Partially Available'), - ('ready', 'Ready'), - ('sent', 'Sent'), - ('to approve', 'To Approve'), - ('approved', 'Approved')], - string='State', + selection=[ + ("draft", "Draft"), + ("assigned", "Assigned"), + ("confirmed", "Confirmed"), + ("waiting", "Waiting"), + ("partially_available", "Partially Available"), + ("ready", "Ready"), + ("sent", "Sent"), + ("to approve", "To Approve"), + ("approved", "Approved"), + ], + string="State", ) stock_move_id = fields.Many2one( - comodel_name='stock.move', - string='Stock Move', index=True, + comodel_name="stock.move", string="Stock Move", index=True ) diff --git a/mrp_multi_level/models/mrp_planned_order.py b/mrp_multi_level/models/mrp_planned_order.py index 9959788c5..2f6734811 100644 --- a/mrp_multi_level/models/mrp_planned_order.py +++ b/mrp_multi_level/models/mrp_planned_order.py @@ -2,7 +2,7 @@ # - Lois Rilo Antelo # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -from odoo import models, fields +from odoo import fields, models class MrpPlannedOrder(models.Model): @@ -26,8 +26,8 @@ class MrpPlannedOrder(models.Model): readonly=True, ) company_id = fields.Many2one( - comodel_name='res.company', - related='product_mrp_area_id.mrp_area_id.warehouse_id.company_id', + comodel_name="res.company", + related="product_mrp_area_id.mrp_area_id.warehouse_id.company_id", store=True, ) product_id = fields.Many2one( @@ -37,12 +37,10 @@ class MrpPlannedOrder(models.Model): readonly=True, ) order_release_date = fields.Date( - string="Release Date", - help="Order release date planned by MRP.", + string="Release Date", help="Order release date planned by MRP." ) due_date = fields.Date( - string="Due Date", - help="Date in which the supply must have been completed.", + string="Due Date", help="Date in which the supply must have been completed." ) qty_released = fields.Float() fixed = fields.Boolean() @@ -55,12 +53,14 @@ class MrpPlannedOrder(models.Model): string="MRP Move DOWN", ) mrp_action = fields.Selection( - selection=[("manufacture", "Manufacturing Order"), - ("buy", "Purchase Order"), - ('pull', 'Pull From'), - ('push', 'Push To'), - ('pull_push', 'Pull & Push'), - ("none", "None")], + selection=[ + ("manufacture", "Manufacturing Order"), + ("buy", "Purchase Order"), + ("pull", "Pull From"), + ("push", "Push To"), + ("pull_push", "Pull & Push"), + ("none", "None"), + ], string="Action", ) mrp_inventory_id = fields.Many2one( diff --git a/mrp_multi_level/models/product_mrp_area.py b/mrp_multi_level/models/product_mrp_area.py index 792279cb4..7165c3ada 100644 --- a/mrp_multi_level/models/product_mrp_area.py +++ b/mrp_multi_level/models/product_mrp_area.py @@ -6,99 +6,89 @@ from math import ceil -from odoo import api, fields, models, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError class ProductMRPArea(models.Model): - _name = 'product.mrp.area' - _description = 'Product MRP Area' + _name = "product.mrp.area" + _description = "Product MRP Area" active = fields.Boolean(default=True) - mrp_area_id = fields.Many2one( - comodel_name='mrp.area', - required=True, - ) + mrp_area_id = fields.Many2one(comodel_name="mrp.area", required=True) company_id = fields.Many2one( - comodel_name='res.company', - related='mrp_area_id.warehouse_id.company_id', + comodel_name="res.company", + related="mrp_area_id.warehouse_id.company_id", store=True, ) product_id = fields.Many2one( - comodel_name='product.product', - required=True, - string='Product', + comodel_name="product.product", required=True, string="Product" ) product_tmpl_id = fields.Many2one( - comodel_name='product.template', + comodel_name="product.template", readonly=True, - related='product_id.product_tmpl_id', + related="product_id.product_tmpl_id", store=True, ) - location_id = fields.Many2one( - related="mrp_area_id.location_id", - ) + location_id = fields.Many2one(related="mrp_area_id.location_id") location_proc_id = fields.Many2one( string="Procure Location", comodel_name="stock.location", domain="[('location_id', 'child_of', location_id)]", help="Set this if you need to procure from a different location" - "than area's location.", + "than area's location.", ) # TODO: applicable and exclude... redundant?? - mrp_applicable = fields.Boolean(string='MRP Applicable') - mrp_exclude = fields.Boolean(string='Exclude from MRP') - mrp_inspection_delay = fields.Integer(string='Inspection Delay') - 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_applicable = fields.Boolean(string="MRP Applicable") + mrp_exclude = fields.Boolean(string="Exclude from MRP") + mrp_inspection_delay = fields.Integer(string="Inspection Delay") + 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(string="Safety Stock") mrp_nbr_days = fields.Integer( - string='Nbr. Days', default=0, + 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 run, in order to determine the quantity to order.", ) - mrp_qty_multiple = fields.Float(string='Qty Multiple', default=1.00) - mrp_transit_delay = fields.Integer(string='Transit Delay', default=0) + mrp_qty_multiple = fields.Float(string="Qty Multiple", default=1.00) + mrp_transit_delay = fields.Integer(string="Transit Delay", default=0) mrp_verified = fields.Boolean( - string='Verified for MRP', + string="Verified for MRP", help="Identifies that this product has been verified " - "to be valid for the MRP.", - ) - mrp_lead_time = fields.Float( - string='Lead Time', - compute='_compute_mrp_lead_time', + "to be valid for the MRP.", ) + mrp_lead_time = fields.Float(string="Lead Time", compute="_compute_mrp_lead_time") main_supplier_id = fields.Many2one( - comodel_name='res.partner', string='Main Supplier', - compute='_compute_main_supplier', store=True, + comodel_name="res.partner", + string="Main Supplier", + compute="_compute_main_supplier", + store=True, index=True, ) main_supplierinfo_id = fields.Many2one( - comodel_name='product.supplierinfo', string='Supplier Info', - compute='_compute_main_supplier', store=True, + comodel_name="product.supplierinfo", + string="Supplier Info", + compute="_compute_main_supplier", + store=True, ) supply_method = fields.Selection( - selection=[('buy', 'Buy'), - ('none', 'Undefined'), - ('manufacture', 'Produce'), - ('pull', 'Pull From'), - ('push', 'Push To'), - ('pull_push', 'Pull & Push')], - string='Supply Method', - compute='_compute_supply_method', + selection=[ + ("buy", "Buy"), + ("none", "Undefined"), + ("manufacture", "Produce"), + ("pull", "Pull From"), + ("push", "Push To"), + ("pull_push", "Pull & Push"), + ], + string="Supply Method", + compute="_compute_supply_method", ) qty_available = fields.Float( - string="Quantity Available", - compute="_compute_qty_available", + string="Quantity Available", compute="_compute_qty_available" ) mrp_move_ids = fields.One2many( - comodel_name='mrp.move', - inverse_name='product_mrp_area_id', - readonly=True, + comodel_name="mrp.move", inverse_name="product_mrp_area_id", readonly=True ) planned_order_ids = fields.One2many( comodel_name="mrp.planned.order", @@ -107,29 +97,41 @@ class ProductMRPArea(models.Model): ) _sql_constraints = [ - ('product_mrp_area_uniq', 'unique(product_id, mrp_area_id)', - 'The product/MRP Area parameters combination must be unique.'), + ( + "product_mrp_area_uniq", + "unique(product_id, mrp_area_id)", + "The product/MRP Area parameters combination must be unique.", + ) ] @api.multi @api.constrains( - "mrp_minimum_order_qty", "mrp_maximum_order_qty", "mrp_qty_multiple", - "mrp_minimum_stock", "mrp_nbr_days", + "mrp_minimum_order_qty", + "mrp_maximum_order_qty", + "mrp_qty_multiple", + "mrp_minimum_stock", + "mrp_nbr_days", ) def _check_negatives(self): - values = self.read([ - "mrp_minimum_order_qty", "mrp_maximum_order_qty", - "mrp_qty_multiple", "mrp_minimum_stock", "mrp_nbr_days", - ]) + values = self.read( + [ + "mrp_minimum_order_qty", + "mrp_maximum_order_qty", + "mrp_qty_multiple", + "mrp_minimum_stock", + "mrp_nbr_days", + ] + ) for rec in values: if any(v < 0 for v in rec.values()): raise ValidationError(_("You cannot use a negative number.")) @api.multi def name_get(self): - return [(area.id, '[%s] %s' % ( - area.mrp_area_id.name, - area.product_id.display_name)) for area in self] + return [ + (area.id, "[{}] {}".format(area.mrp_area_id.name, area.product_id.display_name)) + for area in self + ] @api.multi def _compute_mrp_lead_time(self): @@ -140,36 +142,37 @@ class ProductMRPArea(models.Model): for rec in purchased: rec.mrp_lead_time = rec.main_supplierinfo_id.delay # TODO: 'move' supply method. - for rec in (self - produced - purchased): + for rec in self - produced - purchased: rec.mrp_lead_time = 0 @api.multi def _compute_qty_available(self): for rec in self: rec.qty_available = rec.product_id.with_context( - {'location': rec.mrp_area_id.location_id.id}).qty_available + {"location": rec.mrp_area_id.location_id.id} + ).qty_available @api.multi def _compute_supply_method(self): - group_obj = self.env['procurement.group'] + group_obj = self.env["procurement.group"] for rec in self: proc_loc = rec.location_proc_id or rec.mrp_area_id.location_id values = { - 'warehouse_id': rec.mrp_area_id.warehouse_id, - 'company_id': self.env.user.company_id.id, + "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, proc_loc, values) - rec.supply_method = rule.action if rule else 'none' + rec.supply_method = rule.action if rule else "none" @api.multi - @api.depends('supply_method', 'product_id.route_ids', - 'product_id.seller_ids') + @api.depends("supply_method", "product_id.route_ids", "product_id.seller_ids") def _compute_main_supplier(self): """Simplified and similar to procurement.rule logic.""" - for rec in self.filtered(lambda r: r.supply_method == 'buy'): + 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)) + lambda r: (not r.product_id or r.product_id == rec.product_id) + ) if not suppliers: continue rec.main_supplierinfo_id = suppliers[0] @@ -178,15 +181,17 @@ class ProductMRPArea(models.Model): @api.multi def _adjust_qty_to_order(self, qty_to_order): self.ensure_one() - if (not self.mrp_maximum_order_qty and not - self.mrp_minimum_order_qty and self.mrp_qty_multiple == 1.0): + if ( + not self.mrp_maximum_order_qty + and not self.mrp_minimum_order_qty + and self.mrp_qty_multiple == 1.0 + ): return qty_to_order if qty_to_order < self.mrp_minimum_order_qty: return self.mrp_minimum_order_qty if self.mrp_qty_multiple: multiplier = ceil(qty_to_order / self.mrp_qty_multiple) qty_to_order = multiplier * self.mrp_qty_multiple - if self.mrp_maximum_order_qty and qty_to_order > \ - self.mrp_maximum_order_qty: + if self.mrp_maximum_order_qty and qty_to_order > self.mrp_maximum_order_qty: return self.mrp_maximum_order_qty return qty_to_order diff --git a/mrp_multi_level/models/product_product.py b/mrp_multi_level/models/product_product.py index b52802067..6a5e3be5a 100644 --- a/mrp_multi_level/models/product_product.py +++ b/mrp_multi_level/models/product_product.py @@ -2,33 +2,34 @@ # Copyright 2016-18 Eficent Business and IT Consulting Services S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). import ast + from odoo import api, fields, models class Product(models.Model): - _inherit = 'product.product' + _inherit = "product.product" - llc = fields.Integer(string='Low Level Code', default=0) + llc = fields.Integer(string="Low Level Code", default=0) manufacturing_order_ids = fields.One2many( - comodel_name='mrp.production', - inverse_name='product_id', - string='Manufacturing Orders', - domain=[('state', '=', 'draft')], + comodel_name="mrp.production", + inverse_name="product_id", + string="Manufacturing Orders", + domain=[("state", "=", "draft")], ) purchase_order_line_ids = fields.One2many( - comodel_name='purchase.order.line', - inverse_name='product_id', - string='Purchase Orders', + comodel_name="purchase.order.line", + inverse_name="product_id", + string="Purchase Orders", ) mrp_area_ids = fields.One2many( - comodel_name='product.mrp.area', - inverse_name='product_id', - string='MRP Area parameters' + comodel_name="product.mrp.area", + inverse_name="product_id", + string="MRP Area parameters", ) mrp_area_count = fields.Integer( - string='MRP Area Parameter Count', + string="MRP Area Parameter Count", readonly=True, - compute='_compute_mrp_area_count', + compute="_compute_mrp_area_count", ) @api.multi @@ -39,21 +40,21 @@ class Product(models.Model): @api.multi def action_view_mrp_area_parameters(self): self.ensure_one() - action = self.env.ref('mrp_multi_level.product_mrp_area_action') + action = self.env.ref("mrp_multi_level.product_mrp_area_action") result = action.read()[0] - ctx = ast.literal_eval(result.get('context')) + ctx = ast.literal_eval(result.get("context")) if not ctx: ctx = {} - mrp_areas = self.env['mrp.area'].search([]) + mrp_areas = self.env["mrp.area"].search([]) if len(mrp_areas) == 1: - ctx.update({'default_mrp_area_id': mrp_areas[0].id}) + ctx.update({"default_mrp_area_id": mrp_areas[0].id}) area_ids = self.mrp_area_ids.ids - ctx.update({'default_product_id': self.id}) + ctx.update({"default_product_id": self.id}) if self.mrp_area_count != 1: - result['domain'] = [('id', 'in', area_ids)] + result["domain"] = [("id", "in", area_ids)] else: - res = self.env.ref('mrp_multi_level.product_mrp_area_form', False) - result['views'] = [(res and res.id or False, 'form')] - result['res_id'] = area_ids[0] - result['context'] = ctx + res = self.env.ref("mrp_multi_level.product_mrp_area_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = area_ids[0] + result["context"] = ctx return result diff --git a/mrp_multi_level/models/product_template.py b/mrp_multi_level/models/product_template.py index bcea26a27..e1b128234 100644 --- a/mrp_multi_level/models/product_template.py +++ b/mrp_multi_level/models/product_template.py @@ -1,21 +1,22 @@ # Copyright 2018 Eficent Business and IT Consulting Services S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). import ast + from odoo import api, fields, models class ProductTemplate(models.Model): - _inherit = 'product.template' + _inherit = "product.template" mrp_area_ids = fields.One2many( - comodel_name='product.mrp.area', - inverse_name='product_tmpl_id', - string='MRP Area parameters', + comodel_name="product.mrp.area", + inverse_name="product_tmpl_id", + string="MRP Area parameters", ) mrp_area_count = fields.Integer( - string='MRP Area Parameter Count', + string="MRP Area Parameter Count", readonly=True, - compute='_compute_mrp_area_count', + compute="_compute_mrp_area_count", ) @api.multi @@ -26,24 +27,23 @@ class ProductTemplate(models.Model): @api.multi def action_view_mrp_area_parameters(self): self.ensure_one() - action = self.env.ref('mrp_multi_level.product_mrp_area_action') + action = self.env.ref("mrp_multi_level.product_mrp_area_action") result = action.read()[0] - ctx = ast.literal_eval(result.get('context')) - mrp_areas = self.env['mrp.area'].search([]) - if 'context' not in result: - result['context'] = {} + ctx = ast.literal_eval(result.get("context")) + mrp_areas = self.env["mrp.area"].search([]) + if "context" not in result: + result["context"] = {} if len(mrp_areas) == 1: - ctx.update({'default_mrp_area_id': mrp_areas[0].id}) - mrp_area_ids = self.with_context( - active_test=False).mrp_area_ids.ids + ctx.update({"default_mrp_area_id": mrp_areas[0].id}) + mrp_area_ids = self.with_context(active_test=False).mrp_area_ids.ids if len(self.product_variant_ids) == 1: variant = self.product_variant_ids[0] - ctx.update({'default_product_id': variant.id}) + ctx.update({"default_product_id": variant.id}) if len(mrp_area_ids) != 1: - result['domain'] = [('id', 'in', mrp_area_ids)] + result["domain"] = [("id", "in", mrp_area_ids)] else: - res = self.env.ref('mrp_multi_level.product_mrp_area_form', False) - result['views'] = [(res and res.id or False, 'form')] - result['res_id'] = mrp_area_ids[0] - result['context'] = ctx + res = self.env.ref("mrp_multi_level.product_mrp_area_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = mrp_area_ids[0] + result["context"] = ctx return result diff --git a/mrp_multi_level/models/stock_location.py b/mrp_multi_level/models/stock_location.py index 3c574443c..e07632ac8 100644 --- a/mrp_multi_level/models/stock_location.py +++ b/mrp_multi_level/models/stock_location.py @@ -7,10 +7,11 @@ from odoo import fields, models class StockLocation(models.Model): - _inherit = 'stock.location' + _inherit = "stock.location" mrp_area_id = fields.Many2one( - comodel_name='mrp.area', string='MRP Area', + comodel_name="mrp.area", + string="MRP Area", help="Requirements for a particular MRP area are combined for the " - "purposes of procurement by the MRP.", + "purposes of procurement by the MRP.", ) diff --git a/mrp_multi_level/tests/common.py b/mrp_multi_level/tests/common.py index 8db7567af..591954230 100644 --- a/mrp_multi_level/tests/common.py +++ b/mrp_multi_level/tests/common.py @@ -8,272 +8,330 @@ from odoo.tests.common import SavepointCase class TestMrpMultiLevelCommon(SavepointCase): - @classmethod def setUpClass(cls): super().setUpClass() - cls.mo_obj = cls.env['mrp.production'] - cls.po_obj = cls.env['purchase.order'] - cls.product_obj = cls.env['product.product'] - cls.loc_obj = cls.env['stock.location'] - cls.mrp_area_obj = cls.env['mrp.area'] - cls.product_mrp_area_obj = cls.env['product.mrp.area'] - cls.partner_obj = cls.env['res.partner'] - cls.res_users = cls.env['res.users'] - cls.stock_picking_obj = cls.env['stock.picking'] - cls.mrp_multi_level_wiz = cls.env['mrp.multi.level'] - cls.mrp_inventory_procure_wiz = cls.env['mrp.inventory.procure'] - cls.mrp_inventory_obj = cls.env['mrp.inventory'] - cls.mrp_move_obj = cls.env['mrp.move'] - cls.planned_order_obj = cls.env['mrp.planned.order'] + cls.mo_obj = cls.env["mrp.production"] + cls.po_obj = cls.env["purchase.order"] + cls.product_obj = cls.env["product.product"] + cls.loc_obj = cls.env["stock.location"] + cls.mrp_area_obj = cls.env["mrp.area"] + cls.product_mrp_area_obj = cls.env["product.mrp.area"] + cls.partner_obj = cls.env["res.partner"] + cls.res_users = cls.env["res.users"] + cls.stock_picking_obj = cls.env["stock.picking"] + cls.mrp_multi_level_wiz = cls.env["mrp.multi.level"] + cls.mrp_inventory_procure_wiz = cls.env["mrp.inventory.procure"] + cls.mrp_inventory_obj = cls.env["mrp.inventory"] + cls.mrp_move_obj = cls.env["mrp.move"] + cls.planned_order_obj = cls.env["mrp.planned.order"] - cls.fp_1 = cls.env.ref('mrp_multi_level.product_product_fp_1') - cls.fp_2 = cls.env.ref('mrp_multi_level.product_product_fp_2') - cls.sf_1 = cls.env.ref('mrp_multi_level.product_product_sf_1') - cls.sf_2 = cls.env.ref('mrp_multi_level.product_product_sf_2') - cls.pp_1 = cls.env.ref('mrp_multi_level.product_product_pp_1') - cls.pp_2 = cls.env.ref('mrp_multi_level.product_product_pp_2') - cls.company = cls.env.ref('base.main_company') - cls.mrp_area = cls.env.ref('mrp_multi_level.mrp_area_stock_wh0') - cls.vendor = cls.env.ref('mrp_multi_level.res_partner_lazer_tech') - cls.wh = cls.env.ref('stock.warehouse0') + cls.fp_1 = cls.env.ref("mrp_multi_level.product_product_fp_1") + cls.fp_2 = cls.env.ref("mrp_multi_level.product_product_fp_2") + cls.sf_1 = cls.env.ref("mrp_multi_level.product_product_sf_1") + cls.sf_2 = cls.env.ref("mrp_multi_level.product_product_sf_2") + cls.pp_1 = cls.env.ref("mrp_multi_level.product_product_pp_1") + cls.pp_2 = cls.env.ref("mrp_multi_level.product_product_pp_2") + cls.company = cls.env.ref("base.main_company") + cls.mrp_area = cls.env.ref("mrp_multi_level.mrp_area_stock_wh0") + cls.vendor = cls.env.ref("mrp_multi_level.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') - cls.supplier_location = cls.env.ref('stock.stock_location_suppliers') - cls.calendar = cls.env.ref('resource.resource_calendar_std') + cls.customer_location = cls.env.ref("stock.stock_location_customers") + cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") + cls.calendar = cls.env.ref("resource.resource_calendar_std") # Add calendar to WH: cls.wh.calendar_id = cls.calendar # Partner: - vendor1 = cls.partner_obj.create({'name': 'Vendor 1'}) + vendor1 = cls.partner_obj.create({"name": "Vendor 1"}) # Create user: - group_mrp_manager = cls.env.ref('mrp.group_mrp_manager') - group_user = cls.env.ref('base.group_user') - group_stock_manager = cls.env.ref('stock.group_stock_manager') + group_mrp_manager = cls.env.ref("mrp.group_mrp_manager") + group_user = cls.env.ref("base.group_user") + group_stock_manager = cls.env.ref("stock.group_stock_manager") cls.mrp_manager = cls._create_user( - 'Test User', + "Test User", [group_mrp_manager, group_user, group_stock_manager], cls.company, ) # Create secondary location and MRP Area: - cls.sec_loc = cls.loc_obj.create({ - 'name': 'Test location', - 'usage': 'internal', - 'location_id': cls.wh.view_location_id.id, - }) - cls.secondary_area = cls.mrp_area_obj.create({ - 'name': 'Test', - 'warehouse_id': cls.wh.id, - 'location_id': cls.sec_loc.id, - }) + cls.sec_loc = cls.loc_obj.create( + { + "name": "Test location", + "usage": "internal", + "location_id": cls.wh.view_location_id.id, + } + ) + cls.secondary_area = cls.mrp_area_obj.create( + {"name": "Test", "warehouse_id": cls.wh.id, "location_id": cls.sec_loc.id} + ) # Create an area for design special cases and test them, different # cases will be expected to not share products, this way each case # can be isolated. - cls.cases_loc = cls.loc_obj.create({ - 'name': 'Special Cases location', - 'usage': 'internal', - 'location_id': cls.wh.view_location_id.id, - }) - cls.cases_area = cls.mrp_area_obj.create({ - 'name': 'Special Cases Tests', - 'warehouse_id': cls.wh.id, - 'location_id': cls.cases_loc.id, - }) + cls.cases_loc = cls.loc_obj.create( + { + "name": "Special Cases location", + "usage": "internal", + "location_id": cls.wh.view_location_id.id, + } + ) + cls.cases_area = cls.mrp_area_obj.create( + { + "name": "Special Cases Tests", + "warehouse_id": cls.wh.id, + "location_id": cls.cases_loc.id, + } + ) # Create products: - route_buy = cls.env.ref('purchase_stock.route_warehouse0_buy').id - cls.prod_test = cls.product_obj.create({ - 'name': 'Test Top Seller', - 'type': 'product', - 'list_price': 150.0, - 'produce_delay': 5.0, - 'route_ids': [(6, 0, [route_buy])], - 'seller_ids': [(0, 0, {'name': vendor1.id, 'price': 20.0})], - }) - cls.product_mrp_area_obj.create({ - 'product_id': cls.prod_test.id, - 'mrp_area_id': cls.mrp_area.id, - }) + route_buy = cls.env.ref("purchase_stock.route_warehouse0_buy").id + cls.prod_test = cls.product_obj.create( + { + "name": "Test Top Seller", + "type": "product", + "list_price": 150.0, + "produce_delay": 5.0, + "route_ids": [(6, 0, [route_buy])], + "seller_ids": [(0, 0, {"name": vendor1.id, "price": 20.0})], + } + ) + cls.product_mrp_area_obj.create( + {"product_id": cls.prod_test.id, "mrp_area_id": cls.mrp_area.id} + ) # Parameters in secondary area with nbr_days set. - cls.product_mrp_area_obj.create({ - 'product_id': cls.prod_test.id, - 'mrp_area_id': cls.secondary_area.id, - 'mrp_nbr_days': 7, - }) - cls.prod_min = cls.product_obj.create({ - 'name': 'Product with minimum order qty', - 'type': 'product', - 'list_price': 50.0, - 'route_ids': [(6, 0, [route_buy])], - 'seller_ids': [(0, 0, {'name': vendor1.id, 'price': 10.0})], - }) - cls.product_mrp_area_obj.create({ - 'product_id': cls.prod_min.id, - 'mrp_area_id': cls.mrp_area.id, - 'mrp_minimum_order_qty': 50.0, - 'mrp_maximum_order_qty': 0.0, - 'mrp_qty_multiple': 1.0, - }) + cls.product_mrp_area_obj.create( + { + "product_id": cls.prod_test.id, + "mrp_area_id": cls.secondary_area.id, + "mrp_nbr_days": 7, + } + ) + cls.prod_min = cls.product_obj.create( + { + "name": "Product with minimum order qty", + "type": "product", + "list_price": 50.0, + "route_ids": [(6, 0, [route_buy])], + "seller_ids": [(0, 0, {"name": vendor1.id, "price": 10.0})], + } + ) + cls.product_mrp_area_obj.create( + { + "product_id": cls.prod_min.id, + "mrp_area_id": cls.mrp_area.id, + "mrp_minimum_order_qty": 50.0, + "mrp_maximum_order_qty": 0.0, + "mrp_qty_multiple": 1.0, + } + ) - cls.prod_max = cls.product_obj.create({ - 'name': 'Product with maximum order qty', - 'type': 'product', - 'list_price': 50.0, - 'route_ids': [(6, 0, [route_buy])], - 'seller_ids': [(0, 0, {'name': vendor1.id, 'price': 10.0})], - }) - cls.product_mrp_area_obj.create({ - 'product_id': cls.prod_max.id, - 'mrp_area_id': cls.mrp_area.id, - 'mrp_minimum_order_qty': 50.0, - 'mrp_maximum_order_qty': 100.0, - 'mrp_qty_multiple': 1.0, - }) - cls.prod_multiple = cls.product_obj.create({ - 'name': 'Product with qty multiple', - 'type': 'product', - 'list_price': 50.0, - 'route_ids': [(6, 0, [route_buy])], - 'seller_ids': [(0, 0, {'name': vendor1.id, 'price': 10.0})], - }) - cls.product_mrp_area_obj.create({ - 'product_id': cls.prod_multiple.id, - 'mrp_area_id': cls.mrp_area.id, - 'mrp_minimum_order_qty': 50.0, - 'mrp_maximum_order_qty': 500.0, - 'mrp_qty_multiple': 25.0, - }) + cls.prod_max = cls.product_obj.create( + { + "name": "Product with maximum order qty", + "type": "product", + "list_price": 50.0, + "route_ids": [(6, 0, [route_buy])], + "seller_ids": [(0, 0, {"name": vendor1.id, "price": 10.0})], + } + ) + cls.product_mrp_area_obj.create( + { + "product_id": cls.prod_max.id, + "mrp_area_id": cls.mrp_area.id, + "mrp_minimum_order_qty": 50.0, + "mrp_maximum_order_qty": 100.0, + "mrp_qty_multiple": 1.0, + } + ) + cls.prod_multiple = cls.product_obj.create( + { + "name": "Product with qty multiple", + "type": "product", + "list_price": 50.0, + "route_ids": [(6, 0, [route_buy])], + "seller_ids": [(0, 0, {"name": vendor1.id, "price": 10.0})], + } + ) + cls.product_mrp_area_obj.create( + { + "product_id": cls.prod_multiple.id, + "mrp_area_id": cls.mrp_area.id, + "mrp_minimum_order_qty": 50.0, + "mrp_maximum_order_qty": 500.0, + "mrp_qty_multiple": 25.0, + } + ) # Create more products to test special corner case scenarios: - cls.product_scenario_1 = cls.product_obj.create({ - 'name': 'Product Special Scenario 1', - 'type': 'product', - 'list_price': 100.0, - 'route_ids': [(6, 0, [route_buy])], - 'seller_ids': [(0, 0, {'name': vendor1.id, 'price': 20.0})], - }) - cls.product_mrp_area_obj.create({ - 'product_id': cls.product_scenario_1.id, - 'mrp_area_id': cls.cases_area.id, - 'mrp_nbr_days': 7, - 'mrp_qty_multiple': 5.0, - }) + cls.product_scenario_1 = cls.product_obj.create( + { + "name": "Product Special Scenario 1", + "type": "product", + "list_price": 100.0, + "route_ids": [(6, 0, [route_buy])], + "seller_ids": [(0, 0, {"name": vendor1.id, "price": 20.0})], + } + ) + cls.product_mrp_area_obj.create( + { + "product_id": cls.product_scenario_1.id, + "mrp_area_id": cls.cases_area.id, + "mrp_nbr_days": 7, + "mrp_qty_multiple": 5.0, + } + ) # Create pickings for Scenario 1: dt_base = cls.calendar.plan_days(3 + 1, datetime.today()) cls._create_picking_in( - cls.product_scenario_1, 87, dt_base, location=cls.cases_loc) + cls.product_scenario_1, 87, dt_base, location=cls.cases_loc + ) dt_bit_later = dt_base + timedelta(hours=1) cls._create_picking_out( - cls.product_scenario_1, 124, dt_bit_later, location=cls.cases_loc) + cls.product_scenario_1, 124, dt_bit_later, location=cls.cases_loc + ) dt_base_2 = cls.calendar.plan_days(3 + 1, datetime.today()) cls._create_picking_out( - cls.product_scenario_1, 90, dt_base_2, location=cls.cases_loc) + cls.product_scenario_1, 90, dt_base_2, location=cls.cases_loc + ) dt_next_group = cls.calendar.plan_days(10 + 1, datetime.today()) cls._create_picking_out( - cls.product_scenario_1, 18, dt_next_group, location=cls.cases_loc) + cls.product_scenario_1, 18, dt_next_group, location=cls.cases_loc + ) # Create test picking for FP-1 and FP-2: res = cls.calendar.plan_days(7 + 1, datetime.today().replace(hour=0)) date_move = res.date() - 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 fp-1', - 'product_id': cls.fp_1.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': cls.fp_1.uom_id.id, - 'product_uom_qty': 100, - 'location_id': cls.stock_location.id, - 'location_dest_id': cls.customer_location.id - }), - (0, 0, { - 'name': 'Test move fp-2', - 'product_id': cls.fp_2.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': cls.fp_2.uom_id.id, - 'product_uom_qty': 15, - 'location_id': cls.stock_location.id, - 'location_dest_id': cls.customer_location.id - })] - }) + 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 fp-1", + "product_id": cls.fp_1.id, + "date_expected": date_move, + "date": date_move, + "product_uom": cls.fp_1.uom_id.id, + "product_uom_qty": 100, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + }, + ), + ( + 0, + 0, + { + "name": "Test move fp-2", + "product_id": cls.fp_2.id, + "date_expected": date_move, + "date": date_move, + "product_uom": cls.fp_2.uom_id.id, + "product_uom_qty": 15, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + }, + ), + ], + } + ) cls.picking_1.action_confirm() # Create test picking for procure qty adjustment tests: - cls.picking_2 = 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 prod_min', - 'product_id': cls.prod_min.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': cls.prod_min.uom_id.id, - 'product_uom_qty': 16, - 'location_id': cls.stock_location.id, - 'location_dest_id': cls.customer_location.id - }), - (0, 0, { - 'name': 'Test move prod_max', - 'product_id': cls.prod_max.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': cls.prod_max.uom_id.id, - 'product_uom_qty': 140, - 'location_id': cls.stock_location.id, - 'location_dest_id': cls.customer_location.id - }), - (0, 0, { - 'name': 'Test move prod_multiple', - 'product_id': cls.prod_multiple.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': cls.prod_multiple.uom_id.id, - 'product_uom_qty': 112, - 'location_id': cls.stock_location.id, - 'location_dest_id': cls.customer_location.id - })] - }) + cls.picking_2 = 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 prod_min", + "product_id": cls.prod_min.id, + "date_expected": date_move, + "date": date_move, + "product_uom": cls.prod_min.uom_id.id, + "product_uom_qty": 16, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + }, + ), + ( + 0, + 0, + { + "name": "Test move prod_max", + "product_id": cls.prod_max.id, + "date_expected": date_move, + "date": date_move, + "product_uom": cls.prod_max.uom_id.id, + "product_uom_qty": 140, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + }, + ), + ( + 0, + 0, + { + "name": "Test move prod_multiple", + "product_id": cls.prod_multiple.id, + "date_expected": date_move, + "date": date_move, + "product_uom": cls.prod_multiple.uom_id.id, + "product_uom_qty": 112, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + }, + ), + ], + } + ) cls.picking_2.action_confirm() # Create Test PO: - date_po = cls.calendar.plan_days( - 1+1, datetime.today().replace(hour=0)).date() - 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, - })], - }) + date_po = cls.calendar.plan_days(1 + 1, datetime.today().replace(hour=0)).date() + 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 = cls.calendar.plan_days( - 9 + 1, datetime.today().replace(hour=0)).date() - bom_fp_2 = cls.env.ref('mrp_multi_level.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, - }) + date_mo = cls.calendar.plan_days(9 + 1, datetime.today().replace(hour=0)).date() + bom_fp_2 = cls.env.ref("mrp_multi_level.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, + } + ) # Dates: today = datetime.today().replace(hour=0) @@ -298,55 +356,71 @@ class TestMrpMultiLevelCommon(SavepointCase): @classmethod def create_demand_sec_loc(cls, date_move, qty): - return cls.stock_picking_obj.create({ - "picking_type_id": cls.env.ref("stock.picking_type_out").id, - "location_id": cls.sec_loc.id, - "location_dest_id": cls.customer_location.id, - "move_lines": [ - (0, 0, { - "name": "Test move", - "product_id": cls.prod_test.id, - "date_expected": date_move, - "date": date_move, - "product_uom": cls.prod_test.uom_id.id, - "product_uom_qty": qty, - "location_id": cls.sec_loc.id, - "location_dest_id": cls.customer_location.id, - })], - }) + return cls.stock_picking_obj.create( + { + "picking_type_id": cls.env.ref("stock.picking_type_out").id, + "location_id": cls.sec_loc.id, + "location_dest_id": cls.customer_location.id, + "move_lines": [ + ( + 0, + 0, + { + "name": "Test move", + "product_id": cls.prod_test.id, + "date_expected": date_move, + "date": date_move, + "product_uom": cls.prod_test.uom_id.id, + "product_uom_qty": qty, + "location_id": cls.sec_loc.id, + "location_dest_id": cls.customer_location.id, + }, + ) + ], + } + ) @classmethod def _create_user(cls, login, groups, company): - user = cls.res_users.create({ - 'name': login, - 'login': login, - 'password': 'demo', - 'email': 'example@yourcompany.com', - 'company_id': company.id, - 'groups_id': [(6, 0, [group.id for group in groups])] - }) + user = cls.res_users.create( + { + "name": login, + "login": login, + "password": "demo", + "email": "example@yourcompany.com", + "company_id": company.id, + "groups_id": [(6, 0, [group.id for group in groups])], + } + ) return user @classmethod def _create_picking_in(cls, product, qty, date_move, location=None): if not location: location = cls.stock_location - picking = cls.stock_picking_obj.create({ - 'picking_type_id': cls.env.ref('stock.picking_type_in').id, - 'location_id': cls.supplier_location.id, - 'location_dest_id': location.id, - 'move_lines': [ - (0, 0, { - 'name': 'Test Move', - 'product_id': product.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': product.uom_id.id, - 'product_uom_qty': qty, - 'location_id': cls.supplier_location.id, - 'location_dest_id': location.id, - })], - }) + picking = cls.stock_picking_obj.create( + { + "picking_type_id": cls.env.ref("stock.picking_type_in").id, + "location_id": cls.supplier_location.id, + "location_dest_id": location.id, + "move_lines": [ + ( + 0, + 0, + { + "name": "Test Move", + "product_id": product.id, + "date_expected": date_move, + "date": date_move, + "product_uom": product.uom_id.id, + "product_uom_qty": qty, + "location_id": cls.supplier_location.id, + "location_dest_id": location.id, + }, + ) + ], + } + ) picking.action_confirm() return picking @@ -354,21 +428,28 @@ class TestMrpMultiLevelCommon(SavepointCase): def _create_picking_out(cls, product, qty, date_move, location=None): if not location: location = cls.stock_location - picking = cls.stock_picking_obj.create({ - 'picking_type_id': cls.env.ref('stock.picking_type_out').id, - 'location_id': location.id, - 'location_dest_id': cls.customer_location.id, - 'move_lines': [ - (0, 0, { - 'name': 'Test Move', - 'product_id': product.id, - 'date_expected': date_move, - 'date': date_move, - 'product_uom': product.uom_id.id, - 'product_uom_qty': qty, - 'location_id': location.id, - 'location_dest_id': cls.customer_location.id, - })], - }) + picking = cls.stock_picking_obj.create( + { + "picking_type_id": cls.env.ref("stock.picking_type_out").id, + "location_id": location.id, + "location_dest_id": cls.customer_location.id, + "move_lines": [ + ( + 0, + 0, + { + "name": "Test Move", + "product_id": product.id, + "date_expected": date_move, + "date": date_move, + "product_uom": product.uom_id.id, + "product_uom_qty": qty, + "location_id": location.id, + "location_dest_id": cls.customer_location.id, + }, + ) + ], + } + ) picking.action_confirm() return picking diff --git a/mrp_multi_level/tests/test_mrp_multi_level.py b/mrp_multi_level/tests/test_mrp_multi_level.py index 027d796a0..d7686066a 100644 --- a/mrp_multi_level/tests/test_mrp_multi_level.py +++ b/mrp_multi_level/tests/test_mrp_multi_level.py @@ -2,12 +2,12 @@ # (http://www.eficent.com) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -from odoo.addons.mrp_multi_level.tests.common import TestMrpMultiLevelCommon from odoo import fields +from odoo.addons.mrp_multi_level.tests.common import TestMrpMultiLevelCommon + class TestMrpMultiLevel(TestMrpMultiLevelCommon): - def test_01_mrp_levels(self): """Tests computation of MRP levels.""" self.assertEqual(self.fp_1.llc, 0) @@ -19,51 +19,46 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): def test_02_product_mrp_area(self): """Tests that mrp products are generated correctly.""" - product_mrp_area = self.product_mrp_area_obj.search([ - ('product_id', '=', self.pp_1.id)]) - self.assertEqual(product_mrp_area.supply_method, 'buy') + product_mrp_area = self.product_mrp_area_obj.search( + [("product_id", "=", self.pp_1.id)] + ) + self.assertEqual(product_mrp_area.supply_method, "buy") self.assertEqual(product_mrp_area.main_supplier_id, self.vendor) self.assertEqual(product_mrp_area.qty_available, 10.0) - product_mrp_area = self.product_mrp_area_obj.search([ - ('product_id', '=', self.sf_1.id)]) - self.assertEqual(product_mrp_area.supply_method, 'manufacture') + product_mrp_area = self.product_mrp_area_obj.search( + [("product_id", "=", self.sf_1.id)] + ) + self.assertEqual(product_mrp_area.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), - ]) + moves = self.mrp_move_obj.search([("product_id", "=", self.pp_1.id)]) self.assertEqual(len(moves), 3) - self.assertNotIn('s', moves.mapped('mrp_type')) + self.assertNotIn("s", moves.mapped("mrp_type")) for move in moves: self.assertTrue(move.planned_order_up_ids) - if move.planned_order_up_ids.product_mrp_area_id.product_id == \ - self.fp_1: + if move.planned_order_up_ids.product_mrp_area_id.product_id == self.fp_1: # Demand coming from FP-1 - self.assertEqual( - move.planned_order_up_ids.mrp_action, "manufacture") + self.assertEqual(move.planned_order_up_ids.mrp_action, "manufacture") self.assertEqual(move.mrp_qty, -200.0) - elif move.planned_order_up_ids.product_mrp_area_id.product_id == \ - self.sf_1: + elif move.planned_order_up_ids.product_mrp_area_id.product_id == self.sf_1: # Demand coming from FP-2 -> SF-1 - self.assertEqual( - move.planned_order_up_ids.mrp_action, "manufacture") + self.assertEqual(move.planned_order_up_ids.mrp_action, "manufacture") 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: - planned_orders = self.planned_order_obj.search([ - ('product_id', '=', self.pp_1.id), - ]) + planned_orders = self.planned_order_obj.search( + [("product_id", "=", self.pp_1.id)] + ) self.assertEqual(len(planned_orders), 3) for plan in planned_orders: - self.assertEqual(plan.mrp_action, 'buy') + self.assertEqual(plan.mrp_action, "buy") # Check PP-2 PO being accounted: - po_move = self.mrp_move_obj.search([ - ('product_id', '=', self.pp_2.id), - ('mrp_type', '=', 's'), - ]) + po_move = self.mrp_move_obj.search( + [("product_id", "=", self.pp_2.id), ("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) @@ -72,90 +67,127 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): """Tests MRP inventories created.""" # FP-1 fp_1_inventory_lines = self.mrp_inventory_obj.search( - [('product_mrp_area_id.product_id', '=', self.fp_1.id)]) + [("product_mrp_area_id.product_id", "=", self.fp_1.id)] + ) self.assertEqual(len(fp_1_inventory_lines), 1) 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_line_1 = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.fp_2.id), - ('date', '=', self.date_7)]) + fp_2_line_1 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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([ - ('product_mrp_area_id.product_id', '=', self.fp_2.id), - ('date', '=', self.date_9)]) + fp_2_line_2 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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_line_1 = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.sf_1.id), - ('date', '=', self.date_6)]) + sf_1_line_1 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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([ - ('product_mrp_area_id.product_id', '=', self.sf_1.id), - ('date', '=', self.date_9)]) + sf_1_line_2 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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_line_1 = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.sf_2.id), - ('date', '=', self.date_6)]) + sf_2_line_1 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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([ - ('product_mrp_area_id.product_id', '=', self.sf_2.id), - ('date', '=', self.date_9)]) + sf_2_line_2 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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_line_1 = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.pp_1.id), - ('date', '=', self.date_5)]) + pp_1_line_1 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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([ - ('product_mrp_area_id.product_id', '=', self.pp_1.id), - ('date', '=', self.date_8)]) + pp_1_line_2 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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_obj.search([ - ('product_mrp_area_id.product_id', '=', self.pp_2.id), - ('date', '=', self.date_3)]) + pp_2_line_1 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_id.product_id", "=", self.pp_2.id), + ("date", "=", self.date_3), + ] + ) self.assertEqual(len(pp_2_line_1), 1) 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([ - ('product_mrp_area_id.product_id', '=', self.pp_2.id), - ('date', '=', self.date_5)]) + pp_2_line_2 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_id.product_id", "=", self.pp_2.id), + ("date", "=", self.date_5), + ] + ) self.assertEqual(len(pp_2_line_2), 1) 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([ - ('product_mrp_area_id.product_id', '=', self.pp_2.id), - ('date', '=', self.date_6)]) + pp_2_line_3 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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([ - ('product_mrp_area_id.product_id', '=', self.pp_2.id), - ('date', '=', self.date_8)]) + pp_2_line_4 = self.mrp_inventory_obj.search( + [ + ("product_mrp_area_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) @@ -163,27 +195,28 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): def test_05_planned_availability(self): """Test planned availability computation.""" # Running availability for PP-1: - invs = self.mrp_inventory_obj.search([ - ('product_id', '=', self.pp_1.id)], - order='date') + invs = self.mrp_inventory_obj.search( + [("product_id", "=", self.pp_1.id)], order="date" + ) self.assertEqual(len(invs), 2) expected = [0.0, 0.0] # No grouping, lot size nor safety stock. - self.assertEqual(invs.mapped('running_availability'), expected) + self.assertEqual(invs.mapped("running_availability"), expected) def test_06_procure_mo(self): """Test procurement wizard with MOs.""" - mos = self.mo_obj.search([ - ('product_id', '=', self.fp_1.id)]) + mos = self.mo_obj.search([("product_id", "=", self.fp_1.id)]) self.assertFalse(mos) - mrp_inv = self.mrp_inventory_obj.search([ - ('product_mrp_area_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)]) + mrp_inv = self.mrp_inventory_obj.search( + [("product_mrp_area_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) mo_date_start = fields.Date.to_date(mos.date_planned_start) @@ -193,72 +226,84 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): """Test the adjustments made to the qty to procure when minimum, maximum order quantities and quantity multiple are set.""" # minimum order quantity: - mrp_inv_min = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.prod_min.id)]) + mrp_inv_min = self.mrp_inventory_obj.search( + [("product_mrp_area_id.product_id", "=", self.prod_min.id)] + ) self.assertEqual(mrp_inv_min.to_procure, 50.0) # maximum order quantity: - mrp_inv_max = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.prod_max.id)]) + mrp_inv_max = self.mrp_inventory_obj.search( + [("product_mrp_area_id.product_id", "=", self.prod_max.id)] + ) self.assertEqual(mrp_inv_max.to_procure, 150) - plans = self.planned_order_obj.search([ - ('product_id', '=', self.prod_max.id), - ]) + plans = self.planned_order_obj.search([("product_id", "=", self.prod_max.id)]) self.assertEqual(len(plans), 2) - self.assertIn(100.0, plans.mapped('mrp_qty')) - self.assertIn(50.0, plans.mapped('mrp_qty')) + self.assertIn(100.0, plans.mapped("mrp_qty")) + self.assertIn(50.0, plans.mapped("mrp_qty")) # quantity multiple: - mrp_inv_multiple = self.mrp_inventory_obj.search([ - ('product_mrp_area_id.product_id', '=', self.prod_multiple.id)]) + mrp_inv_multiple = self.mrp_inventory_obj.search( + [("product_mrp_area_id.product_id", "=", self.prod_multiple.id)] + ) self.assertEqual(mrp_inv_multiple.to_procure, 125) def test_08_group_demand(self): """Test demand grouping functionality, `nbr_days`.""" - pickings = self.stock_picking_obj.search([ - ('product_id', '=', self.prod_test.id), - ('location_id', '=', self.sec_loc.id)]) + pickings = self.stock_picking_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("location_id", "=", self.sec_loc.id), + ] + ) self.assertEqual(len(pickings), 5) - moves = self.mrp_move_obj.search([ - ('product_id', '=', self.prod_test.id), - ('mrp_area_id', '=', self.secondary_area.id), - ]) - supply_plans = self.planned_order_obj.search([ - ('product_id', '=', self.prod_test.id), - ('mrp_area_id', '=', self.secondary_area.id), - ]) - moves_demand = moves.filtered(lambda m: m.mrp_type == 'd') + moves = self.mrp_move_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.secondary_area.id), + ] + ) + supply_plans = self.planned_order_obj.search( + [ + ("product_id", "=", self.prod_test.id), + ("mrp_area_id", "=", self.secondary_area.id), + ] + ) + moves_demand = moves.filtered(lambda m: m.mrp_type == "d") self.assertEqual(len(moves_demand), 5) # two groups expected: # 1. days 8, 9 and 10. # 2. days 20, and 22. self.assertEqual(len(supply_plans), 2) - quantities = supply_plans.mapped('mrp_qty') - week_1_expected = sum(moves_demand[0:3].mapped('mrp_qty')) + quantities = supply_plans.mapped("mrp_qty") + week_1_expected = sum(moves_demand[0:3].mapped("mrp_qty")) self.assertIn(abs(week_1_expected), quantities) - week_2_expected = sum(moves_demand[3:].mapped('mrp_qty')) + week_2_expected = sum(moves_demand[3:].mapped("mrp_qty")) self.assertIn(abs(week_2_expected), quantities) def test_09_isolated_mrp_area_run(self): """Test running MRP for just one area.""" - self.mrp_multi_level_wiz.sudo(self.mrp_manager).create({ - 'mrp_area_ids': [(6, 0, self.secondary_area.ids)], - }).run_mrp_multi_level() - this = self.mrp_inventory_obj.search([ - ('mrp_area_id', '=', self.secondary_area.id)], limit=1) + self.mrp_multi_level_wiz.sudo(self.mrp_manager).create( + {"mrp_area_ids": [(6, 0, self.secondary_area.ids)]} + ).run_mrp_multi_level() + this = self.mrp_inventory_obj.search( + [("mrp_area_id", "=", self.secondary_area.id)], limit=1 + ) self.assertTrue(this) # Only recently exectued areas should have been created by test user: self.assertEqual(this.create_uid, self.mrp_manager) - prev = self.mrp_inventory_obj.search([ - ('mrp_area_id', '!=', self.secondary_area.id)], limit=1) + prev = self.mrp_inventory_obj.search( + [("mrp_area_id", "!=", self.secondary_area.id)], limit=1 + ) self.assertNotEqual(this.create_uid, prev.create_uid) def test_11_special_scenario_1(self): """When grouping demand supply and demand are in the same day but supply goes first.""" - moves = self.mrp_move_obj.search([ - ('product_id', '=', self.product_scenario_1.id)]) + moves = self.mrp_move_obj.search( + [("product_id", "=", self.product_scenario_1.id)] + ) self.assertEqual(len(moves), 4) - mrp_invs = self.mrp_inventory_obj.search([ - ('product_id', '=', self.product_scenario_1.id)]) + mrp_invs = self.mrp_inventory_obj.search( + [("product_id", "=", self.product_scenario_1.id)] + ) self.assertEqual(len(mrp_invs), 2) # Net needs = 124 + 90 - 87 = 127 -> 130 (because of qty multiple) self.assertEqual(mrp_invs[0].to_procure, 130) diff --git a/mrp_multi_level/wizards/mrp_inventory_procure.py b/mrp_multi_level/wizards/mrp_inventory_procure.py index e107a6ba6..a74bc4c3a 100644 --- a/mrp_multi_level/wizards/mrp_inventory_procure.py +++ b/mrp_multi_level/wizards/mrp_inventory_procure.py @@ -2,68 +2,66 @@ # (http://www.eficent.com) # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -from odoo import api, fields, models, _ +from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError class MrpInventoryProcure(models.TransientModel): - _name = 'mrp.inventory.procure' - _description = 'Make Procurements from MRP inventory projections' + _name = "mrp.inventory.procure" + _description = "Make Procurements from MRP inventory projections" item_ids = fields.One2many( - comodel_name='mrp.inventory.procure.item', - inverse_name='wiz_id', - string='Items', + comodel_name="mrp.inventory.procure.item", inverse_name="wiz_id", string="Items" ) @api.model def _prepare_item(self, planned_order): return { - 'planned_order_id': planned_order.id, - 'qty': planned_order.mrp_qty - planned_order.qty_released, - 'uom_id': planned_order.mrp_inventory_id.uom_id.id, - 'date_planned': planned_order.due_date, - 'mrp_inventory_id': planned_order.mrp_inventory_id.id, - 'product_id': planned_order.product_id.id, - 'warehouse_id': planned_order.mrp_area_id.warehouse_id.id, - 'location_id': - planned_order.product_mrp_area_id.location_proc_id.id or - planned_order.mrp_area_id.location_id.id, + "planned_order_id": planned_order.id, + "qty": planned_order.mrp_qty - planned_order.qty_released, + "uom_id": planned_order.mrp_inventory_id.uom_id.id, + "date_planned": planned_order.due_date, + "mrp_inventory_id": planned_order.mrp_inventory_id.id, + "product_id": planned_order.product_id.id, + "warehouse_id": planned_order.mrp_area_id.warehouse_id.id, + "location_id": planned_order.product_mrp_area_id.location_proc_id.id + or planned_order.mrp_area_id.location_id.id, } @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, - submenu=False): - if self.user_has_groups( - "mrp_multi_level.group_change_mrp_procure_qty"): + def fields_view_get( + self, view_id=None, view_type="form", toolbar=False, submenu=False + ): + if self.user_has_groups("mrp_multi_level.group_change_mrp_procure_qty"): view_id = self.env.ref( - 'mrp_multi_level.' - 'view_mrp_inventory_procure_wizard').id + "mrp_multi_level." "view_mrp_inventory_procure_wizard" + ).id else: view_id = self.env.ref( - 'mrp_multi_level.' - 'view_mrp_inventory_procure_without_security').id + "mrp_multi_level." "view_mrp_inventory_procure_without_security" + ).id return super(MrpInventoryProcure, self).fields_view_get( - view_id=view_id, view_type=view_type, toolbar=toolbar, - submenu=submenu) + view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu + ) @api.model def default_get(self, fields): res = super(MrpInventoryProcure, self).default_get(fields) - mrp_inventory_obj = self.env['mrp.inventory'] - mrp_inventory_ids = self.env.context['active_ids'] or [] - active_model = self.env.context['active_model'] - if not mrp_inventory_ids or 'item_ids' not in fields: + mrp_inventory_obj = self.env["mrp.inventory"] + mrp_inventory_ids = self.env.context["active_ids"] or [] + active_model = self.env.context["active_model"] + if not mrp_inventory_ids or "item_ids" not in fields: return res - assert active_model == 'mrp.inventory', 'Bad context propagation' + assert active_model == "mrp.inventory", "Bad context propagation" - items = item_obj = self.env['mrp.inventory.procure.item'] + items = item_obj = self.env["mrp.inventory.procure.item"] for line in mrp_inventory_obj.browse(mrp_inventory_ids).mapped( - 'planned_order_ids'): + "planned_order_ids" + ): if line.qty_released < line.mrp_qty: items += item_obj.create(self._prepare_item(line)) - res['item_ids'] = [(6, 0, items.ids)] + res["item_ids"] = [(6, 0, items.ids)] return res @api.multi @@ -76,69 +74,58 @@ class MrpInventoryProcure(models.TransientModel): values = item._prepare_procurement_values() # Run procurement try: - self.env['procurement.group'].run( + self.env["procurement.group"].run( item.product_id, item.qty, item.uom_id, item.location_id, - 'INT: ' + str(self.env.user.login), # name? - 'INT: ' + str(self.env.user.login), # origin? - values + "INT: " + str(self.env.user.login), # name? + "INT: " + str(self.env.user.login), # origin? + values, ) item.planned_order_id.qty_released += item.qty except UserError as error: errors.append(error.name) if errors: - raise UserError('\n'.join(errors)) - return {'type': 'ir.actions.act_window_close'} + raise UserError("\n".join(errors)) + return {"type": "ir.actions.act_window_close"} class MrpInventoryProcureItem(models.TransientModel): - _name = 'mrp.inventory.procure.item' + _name = "mrp.inventory.procure.item" _description = "MRP Inventory procure item" wiz_id = fields.Many2one( - comodel_name='mrp.inventory.procure', string='Wizard', - ondelete='cascade', readonly=True, + comodel_name="mrp.inventory.procure", + string="Wizard", + ondelete="cascade", + readonly=True, ) - qty = fields.Float(string='Quantity') - uom_id = fields.Many2one( - string='Unit of Measure', - comodel_name='uom.uom', - ) - date_planned = fields.Date(string='Planned Date', required=False) + qty = fields.Float(string="Quantity") + uom_id = fields.Many2one(string="Unit of Measure", comodel_name="uom.uom") + date_planned = fields.Date(string="Planned Date", required=False) mrp_inventory_id = fields.Many2one( - string='Mrp Inventory', - comodel_name='mrp.inventory', - ) - planned_order_id = fields.Many2one( - comodel_name='mrp.planned.order', - ) - product_id = fields.Many2one( - string='Product', - comodel_name='product.product', - ) - warehouse_id = fields.Many2one( - string='Warehouse', - comodel_name='stock.warehouse', - ) - location_id = fields.Many2one( - string='Location', - comodel_name='stock.location', + string="Mrp Inventory", comodel_name="mrp.inventory" ) + planned_order_id = fields.Many2one(comodel_name="mrp.planned.order") + product_id = fields.Many2one(string="Product", comodel_name="product.product") + warehouse_id = fields.Many2one(string="Warehouse", comodel_name="stock.warehouse") + location_id = fields.Many2one(string="Location", comodel_name="stock.location") def _prepare_procurement_values(self, group=False): return { - 'date_planned': fields.Datetime.to_string( - fields.Date.from_string(self.date_planned)), - 'warehouse_id': self.warehouse_id, + "date_planned": fields.Datetime.to_string( + fields.Date.from_string(self.date_planned) + ), + "warehouse_id": self.warehouse_id, # 'company_id': self.company_id, # TODO: consider company - 'group_id': group, + "group_id": group, } @api.multi - @api.onchange('uom_id') + @api.onchange("uom_id") def onchange_uom_id(self): for rec in self: rec.qty = rec.mrp_inventory_id.uom_id._compute_quantity( - rec.mrp_inventory_id.to_procure, rec.uom_id) + rec.mrp_inventory_id.to_procure, rec.uom_id + ) diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index 301640de9..18b5fd60d 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -4,14 +4,16 @@ # - Lois Rilo # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). -from odoo import api, fields, models, exceptions, _ -from datetime import date, timedelta import logging +from datetime import date, timedelta + +from odoo import _, api, exceptions, fields, models + logger = logging.getLogger(__name__) class MultiLevelMrp(models.TransientModel): - _name = 'mrp.multi.level' + _name = "mrp.multi.level" _description = "Multi Level MRP" mrp_area_ids = fields.Many2many( @@ -25,39 +27,40 @@ class MultiLevelMrp(models.TransientModel): @api.model def _prepare_product_mrp_area_data(self, product_mrp_area): qty_available = 0.0 - product_obj = self.env['product.product'] + product_obj = self.env["product.product"] location_ids = product_mrp_area.mrp_area_id._get_locations() for location in location_ids: - product_l = product_obj.with_context( - {'location': location.id}).browse( - product_mrp_area.product_id.id) + product_l = product_obj.with_context({"location": location.id}).browse( + product_mrp_area.product_id.id + ) qty_available += product_l.qty_available return { - 'product_mrp_area_id': product_mrp_area.id, - 'mrp_qty_available': qty_available, - 'mrp_llc': product_mrp_area.product_id.llc, + "product_mrp_area_id": product_mrp_area.id, + "mrp_qty_available": qty_available, + "mrp_llc": product_mrp_area.product_id.llc, } @api.model def _prepare_mrp_move_data_from_stock_move( - self, product_mrp_area, move, direction='in'): - if direction == 'out': - mrp_type = 'd' + self, product_mrp_area, move, direction="in" + ): + if direction == "out": + mrp_type = "d" product_qty = -move.product_qty else: - mrp_type = 's' + mrp_type = "s" product_qty = move.product_qty po = po_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' + 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' + origin = "mo" mo = move.production_id.id else: if move.move_dest_ids: @@ -65,11 +68,10 @@ class MultiLevelMrp(models.TransientModel): 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' + 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 + 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: @@ -78,69 +80,68 @@ class MultiLevelMrp(models.TransientModel): if move.date_expected.date() > date.today(): mrp_date = move.date_expected.date() return { - 'product_id': move.product_id.id, - 'product_mrp_area_id': product_mrp_area.id, - 'production_id': mo, - 'purchase_order_id': po, - 'purchase_line_id': po_line, - 'stock_move_id': move.id, - 'mrp_qty': product_qty, - 'current_qty': product_qty, - 'mrp_date': mrp_date, - 'current_date': move.date_expected, - 'mrp_type': mrp_type, - 'mrp_origin': origin, - 'mrp_order_number': order_number, - 'parent_product_id': parent_product_id, - 'name': order_number, - 'state': move.state, + "product_id": move.product_id.id, + "product_mrp_area_id": product_mrp_area.id, + "production_id": mo, + "purchase_order_id": po, + "purchase_line_id": po_line, + "stock_move_id": move.id, + "mrp_qty": product_qty, + "current_qty": product_qty, + "mrp_date": mrp_date, + "current_date": move.date_expected, + "mrp_type": mrp_type, + "mrp_origin": origin, + "mrp_order_number": order_number, + "parent_product_id": parent_product_id, + "name": order_number, + "state": move.state, } @api.model def _prepare_planned_order_data( - self, product_mrp_area, qty, mrp_date_supply, - mrp_action_date, name + self, product_mrp_area, qty, mrp_date_supply, mrp_action_date, name ): return { - 'product_mrp_area_id': product_mrp_area.id, - 'mrp_qty': qty, - 'due_date': mrp_date_supply, - 'order_release_date': mrp_action_date, - 'mrp_action': product_mrp_area.supply_method, - 'qty_released': 0.0, - 'name': 'Supply: ' + name, + "product_mrp_area_id": product_mrp_area.id, + "mrp_qty": qty, + "due_date": mrp_date_supply, + "order_release_date": mrp_action_date, + "mrp_action": product_mrp_area.supply_method, + "qty_released": 0.0, + "name": "Supply: " + name, } @api.model def _prepare_mrp_move_data_bom_explosion( - self, product, bomline, qty, mrp_date_demand_2, bom, name): + self, product, bomline, qty, mrp_date_demand_2, bom, name + ): product_mrp_area = self._get_product_mrp_area_from_product_and_area( - bomline.product_id, product.mrp_area_id) + bomline.product_id, product.mrp_area_id + ) if not product_mrp_area: - raise exceptions.Warning( - _("No MRP product found")) + raise exceptions.Warning(_("No MRP product found")) return { - 'mrp_area_id': product.mrp_area_id.id, - 'product_id': bomline.product_id.id, - 'product_mrp_area_id': product_mrp_area.id, - 'production_id': None, - 'purchase_order_id': None, - 'purchase_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_type': 'd', - '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: '), + "mrp_area_id": product.mrp_area_id.id, + "product_id": bomline.product_id.id, + "product_mrp_area_id": product_mrp_area.id, + "production_id": None, + "purchase_order_id": None, + "purchase_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_type": "d", + "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 @@ -158,18 +159,14 @@ class MultiLevelMrp(models.TransientModel): date_str = fields.Date.to_string(mrp_date) dt = fields.Datetime.from_string(date_str) # dt is at the beginning of the day (00:00) - res = calendar.plan_days( - -1 * product_mrp_area.mrp_lead_time, dt) + res = calendar.plan_days(-1 * product_mrp_area.mrp_lead_time, dt) mrp_action_date = res.date() else: - mrp_action_date = mrp_date - timedelta( - days=product_mrp_area.mrp_lead_time) + mrp_action_date = mrp_date - timedelta(days=product_mrp_area.mrp_lead_time) return mrp_action_date, mrp_date_supply @api.model - def explode_action( - self, product_mrp_area_id, mrp_action_date, name, qty, action - ): + def explode_action(self, product_mrp_area_id, mrp_action_date, name, qty, action): """Explode requirements.""" mrp_date_demand = mrp_action_date if mrp_date_demand < date.today(): @@ -184,52 +181,56 @@ class MultiLevelMrp(models.TransientModel): if bomcount != 1: continue for bomline in bom.bom_line_ids: - if bomline.product_qty <= 0.00 or \ - bomline.product_id.type != 'product': + if bomline.product_qty <= 0.00 or bomline.product_id.type != "product": continue if self.with_context(mrp_explosion=True)._exclude_from_mrp( - bomline.product_id, - product_mrp_area_id.mrp_area_id): + bomline.product_id, product_mrp_area_id.mrp_area_id + ): # Stop explosion. continue # TODO: review: mrp_transit_delay, mrp_inspection_delay mrp_date_demand_2 = mrp_date_demand - timedelta( - days=(product_mrp_area_id.mrp_transit_delay + - product_mrp_area_id.mrp_inspection_delay)) - move_data = \ - self._prepare_mrp_move_data_bom_explosion( - product_mrp_area_id, bomline, qty, - mrp_date_demand_2, - bom, name) - mrpmove_id2 = self.env['mrp.move'].create(move_data) + days=( + product_mrp_area_id.mrp_transit_delay + + product_mrp_area_id.mrp_inspection_delay + ) + ) + move_data = self._prepare_mrp_move_data_bom_explosion( + product_mrp_area_id, bomline, qty, mrp_date_demand_2, bom, name + ) + mrpmove_id2 = self.env["mrp.move"].create(move_data) if hasattr(action, "mrp_move_down_ids"): action.mrp_move_down_ids = [(4, mrpmove_id2.id)] return True @api.model - def create_action( - self, product_mrp_area_id, mrp_date, mrp_qty, name, values=None, - ): + def create_action(self, product_mrp_area_id, mrp_date, mrp_qty, name, values=None): if not values: values = {} if not isinstance(mrp_date, date): mrp_date = fields.Date.from_string(mrp_date) - action_date, date_supply = \ - self._get_action_and_supply_dates(product_mrp_area_id, mrp_date) + action_date, date_supply = self._get_action_and_supply_dates( + product_mrp_area_id, mrp_date + ) return self.create_planned_order( - product_mrp_area_id, mrp_qty, name, date_supply, - action_date, values=values) + product_mrp_area_id, mrp_qty, name, date_supply, action_date, values=values + ) @api.model def create_planned_order( - self, product_mrp_area_id, mrp_qty, - name, mrp_date_supply, mrp_action_date, values=None, + self, + product_mrp_area_id, + mrp_qty, + name, + mrp_date_supply, + mrp_action_date, + values=None, ): self = self.with_context(auditlog_disabled=True) if self._exclude_from_mrp( - product_mrp_area_id.product_id, - product_mrp_area_id.mrp_area_id): - values['qty_ordered'] = 0.0 + product_mrp_area_id.product_id, product_mrp_area_id.mrp_area_id + ): + values["qty_ordered"] = 0.0 return values qty_ordered = values.get("qty_ordered", 0.0) if values else 0.0 @@ -238,21 +239,21 @@ class MultiLevelMrp(models.TransientModel): qty = product_mrp_area_id._adjust_qty_to_order(qty_to_order) qty_to_order -= qty order_data = self._prepare_planned_order_data( - product_mrp_area_id, qty, mrp_date_supply, mrp_action_date, - name) - planned_order = self.env['mrp.planned.order'].create(order_data) + product_mrp_area_id, qty, mrp_date_supply, mrp_action_date, name + ) + planned_order = self.env["mrp.planned.order"].create(order_data) qty_ordered = qty_ordered + qty - if product_mrp_area_id.supply_method == 'manufacture': + if product_mrp_area_id.supply_method == "manufacture": self.explode_action( - product_mrp_area_id, mrp_action_date, - name, qty, planned_order) + product_mrp_area_id, mrp_action_date, name, qty, planned_order + ) - values['qty_ordered'] = qty_ordered - log_msg = '[%s] %s: qty_ordered = %s' % ( + values["qty_ordered"] = qty_ordered + log_msg = "[{}] {}: qty_ordered = {}".format( product_mrp_area_id.mrp_area_id.name, - product_mrp_area_id.product_id.default_code or - product_mrp_area_id.product_id.name, + product_mrp_area_id.product_id.default_code + or product_mrp_area_id.product_id.name, qty_ordered, ) logger.debug(log_msg) @@ -260,47 +261,48 @@ class MultiLevelMrp(models.TransientModel): @api.model def _mrp_cleanup(self, mrp_areas): - logger.info('Start MRP Cleanup') + logger.info("Start MRP Cleanup") domain = [] if mrp_areas: - domain += [('mrp_area_id', 'in', mrp_areas.ids)] - self.env['mrp.move'].search(domain).unlink() - self.env['mrp.inventory'].search(domain).unlink() - domain += [('fixed', '=', False)] - self.env['mrp.planned.order'].search(domain).unlink() - logger.info('End MRP Cleanup') + domain += [("mrp_area_id", "in", mrp_areas.ids)] + self.env["mrp.move"].search(domain).unlink() + self.env["mrp.inventory"].search(domain).unlink() + domain += [("fixed", "=", False)] + self.env["mrp.planned.order"].search(domain).unlink() + logger.info("End MRP Cleanup") return True @api.model def _low_level_code_calculation(self): - logger.info('Start low level code calculation') + logger.info("Start low level code calculation") counter = 999999 llc = 0 - self.env['product.product'].search([]).write({'llc': llc}) - products = self.env['product.product'].search([('llc', '=', llc)]) + self.env["product.product"].search([]).write({"llc": llc}) + products = self.env["product.product"].search([("llc", "=", llc)]) if products: counter = len(products) - log_msg = 'Low level code 0 finished - Nbr. products: %s' % counter + log_msg = "Low level code 0 finished - Nbr. products: %s" % counter logger.info(log_msg) while counter: llc += 1 - products = self.env['product.product'].search( - [('llc', '=', llc - 1)]) - p_templates = products.mapped('product_tmpl_id') - bom_lines = self.env['mrp.bom.line'].search( - [('product_id.llc', '=', llc - 1), - ('bom_id.product_tmpl_id', 'in', p_templates.ids)]) - products = bom_lines.mapped('product_id') - products.write({'llc': llc}) - products = self.env['product.product'].search([('llc', '=', llc)]) + products = self.env["product.product"].search([("llc", "=", llc - 1)]) + p_templates = products.mapped("product_tmpl_id") + bom_lines = self.env["mrp.bom.line"].search( + [ + ("product_id.llc", "=", llc - 1), + ("bom_id.product_tmpl_id", "in", p_templates.ids), + ] + ) + products = bom_lines.mapped("product_id") + products.write({"llc": llc}) + products = self.env["product.product"].search([("llc", "=", llc)]) counter = len(products) - log_msg = 'Low level code %s finished - Nbr. products: %s' % ( - llc, counter) + log_msg = "Low level code {} finished - Nbr. products: {}".format(llc, counter) logger.info(log_msg) mrp_lowest_llc = llc - logger.info('End low level code calculation') + logger.info("End low level code calculation") return mrp_lowest_llc @api.model @@ -312,21 +314,19 @@ class MultiLevelMrp(models.TransientModel): @api.model def _calculate_mrp_applicable(self, mrp_areas): - logger.info('Start Calculate MRP Applicable') + logger.info("Start Calculate MRP Applicable") domain = [] if mrp_areas: - domain += [('mrp_area_id', 'in', mrp_areas.ids)] - self.env['product.mrp.area'].search(domain).write( - {'mrp_applicable': False}) - domain += [('product_id.type', '=', 'product')] - self.env['product.mrp.area'].search(domain).write( - {'mrp_applicable': True}) + domain += [("mrp_area_id", "in", mrp_areas.ids)] + self.env["product.mrp.area"].search(domain).write({"mrp_applicable": False}) + domain += [("product_id.type", "=", "product")] + self.env["product.mrp.area"].search(domain).write({"mrp_applicable": True}) self._adjust_mrp_applicable(mrp_areas) - count_domain = [('mrp_applicable', '=', True)] + count_domain = [("mrp_applicable", "=", True)] if mrp_areas: - count_domain += [('mrp_area_id', 'in', mrp_areas.ids)] - counter = self.env['product.mrp.area'].search(count_domain, count=True) - log_msg = 'End Calculate MRP Applicable: %s' % counter + count_domain += [("mrp_area_id", "in", mrp_areas.ids)] + counter = self.env["product.mrp.area"].search(count_domain, count=True) + log_msg = "End Calculate MRP Applicable: %s" % counter logger.info(log_msg) return True @@ -341,28 +341,28 @@ class MultiLevelMrp(models.TransientModel): def _in_stock_moves_domain(self, product_mrp_area): locations = product_mrp_area.mrp_area_id._get_locations() return [ - ('product_id', '=', product_mrp_area.product_id.id), - ('state', 'not in', ['done', 'cancel']), - ('product_qty', '>', 0.00), - ('location_id', 'not in', locations.ids), - ('location_dest_id', 'in', locations.ids), + ("product_id", "=", product_mrp_area.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, product_mrp_area): locations = product_mrp_area.mrp_area_id._get_locations() return [ - ('product_id', '=', product_mrp_area.product_id.id), - ('state', 'not in', ['done', 'cancel']), - ('product_qty', '>', 0.00), - ('location_id', 'in', locations.ids), - ('location_dest_id', 'not in', locations.ids), + ("product_id", "=", product_mrp_area.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, product_mrp_area): - move_obj = self.env['stock.move'] - mrp_move_obj = self.env['mrp.move'] + move_obj = self.env["stock.move"] + mrp_move_obj = self.env["mrp.move"] in_domain = self._in_stock_moves_domain(product_mrp_area) in_moves = move_obj.search(in_domain) out_domain = self._out_stock_moves_domain(product_mrp_area) @@ -370,67 +370,74 @@ class MultiLevelMrp(models.TransientModel): if in_moves: for move in in_moves: move_data = self._prepare_mrp_move_data_from_stock_move( - product_mrp_area, move, direction='in') + product_mrp_area, 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( - product_mrp_area, move, direction='out') + product_mrp_area, move, direction="out" + ) mrp_move_obj.create(move_data) return True @api.model - def _prepare_mrp_move_data_from_purchase_order( - self, poline, product_mrp_area): + def _prepare_mrp_move_data_from_purchase_order(self, poline, product_mrp_area): mrp_date = date.today() if fields.Date.from_string(poline.date_planned) > date.today(): mrp_date = fields.Date.from_string(poline.date_planned) return { - 'product_id': poline.product_id.id, - 'product_mrp_area_id': product_mrp_area.id, - 'production_id': None, - 'purchase_order_id': poline.order_id.id, - 'purchase_line_id': poline.id, - 'stock_move_id': None, - 'mrp_qty': poline.product_qty, - 'current_qty': poline.product_qty, - 'mrp_date': mrp_date, - 'current_date': poline.date_planned, - 'mrp_type': 's', - 'mrp_origin': 'po', - 'mrp_order_number': poline.order_id.name, - 'parent_product_id': None, - 'name': poline.order_id.name, - 'state': poline.order_id.state, + "product_id": poline.product_id.id, + "product_mrp_area_id": product_mrp_area.id, + "production_id": None, + "purchase_order_id": poline.order_id.id, + "purchase_line_id": poline.id, + "stock_move_id": None, + "mrp_qty": poline.product_qty, + "current_qty": poline.product_qty, + "mrp_date": mrp_date, + "current_date": poline.date_planned, + "mrp_type": "s", + "mrp_origin": "po", + "mrp_order_number": poline.order_id.name, + "parent_product_id": None, + "name": poline.order_id.name, + "state": poline.order_id.state, } @api.model def _init_mrp_move_from_purchase_order(self, product_mrp_area): location_ids = product_mrp_area.mrp_area_id._get_locations() - picking_types = self.env['stock.picking.type'].search( - [('default_location_dest_id', 'in', - location_ids.ids)]) + picking_types = self.env["stock.picking.type"].search( + [("default_location_dest_id", "in", location_ids.ids)] + ) 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', 'sent', 'to approve'])]) - po_lines = self.env['purchase.order.line'].search( - [('order_id', 'in', orders.ids), - ('product_qty', '>', 0.0), - ('product_id', '=', product_mrp_area.product_id.id)]) + orders = self.env["purchase.order"].search( + [ + ("picking_type_id", "in", picking_type_ids), + ("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", "=", product_mrp_area.product_id.id), + ] + ) for line in po_lines: - mrp_move_data = \ - self._prepare_mrp_move_data_from_purchase_order( - line, product_mrp_area) - self.env['mrp.move'].create(mrp_move_data) + mrp_move_data = self._prepare_mrp_move_data_from_purchase_order( + line, product_mrp_area + ) + self.env["mrp.move"].create(mrp_move_data) @api.model def _get_product_mrp_area_from_product_and_area(self, product, mrp_area): - return self.env['product.mrp.area'].search([ - ('product_id', '=', product.id), - ('mrp_area_id', '=', mrp_area.id), - ], limit=1) + return self.env["product.mrp.area"].search( + [("product_id", "=", product.id), ("mrp_area_id", "=", mrp_area.id)], + limit=1, + ) @api.model def _init_mrp_move(self, product_mrp_area): @@ -441,35 +448,37 @@ class MultiLevelMrp(models.TransientModel): @api.model def _exclude_from_mrp(self, product, mrp_area): """ To extend with various logic where needed. """ - product_mrp_area = self.env['product.mrp.area'].search( - [('product_id', '=', product.id), - ('mrp_area_id', '=', mrp_area.id)], limit=1) + product_mrp_area = self.env["product.mrp.area"].search( + [("product_id", "=", product.id), ("mrp_area_id", "=", mrp_area.id)], + limit=1, + ) if not product_mrp_area: return True return product_mrp_area.mrp_exclude @api.model def _mrp_initialisation(self, mrp_areas): - logger.info('Start MRP initialisation') + logger.info("Start MRP initialisation") if not mrp_areas: - mrp_areas = self.env['mrp.area'].search([]) - product_mrp_areas = self.env['product.mrp.area'].search([ - ('mrp_area_id', 'in', mrp_areas.ids), - ('mrp_applicable', '=', True), - ]) + mrp_areas = self.env["mrp.area"].search([]) + product_mrp_areas = self.env["product.mrp.area"].search( + [("mrp_area_id", "in", mrp_areas.ids), ("mrp_applicable", "=", True)] + ) init_counter = 0 for mrp_area in mrp_areas: for product_mrp_area in product_mrp_areas.filtered( - lambda a: a.mrp_area_id == mrp_area): - if self._exclude_from_mrp( - product_mrp_area.product_id, mrp_area): + lambda a: a.mrp_area_id == mrp_area + ): + if self._exclude_from_mrp(product_mrp_area.product_id, mrp_area): continue init_counter += 1 - log_msg = 'MRP Init: %s - %s ' % ( - init_counter, product_mrp_area.display_name) + log_msg = "MRP Init: {} - {} ".format( + init_counter, + product_mrp_area.display_name, + ) logger.info(log_msg) self._init_mrp_move(product_mrp_area) - logger.info('End MRP initialisation') + logger.info("End MRP initialisation") @api.model def _init_mrp_move_grouped_demand(self, nbr_create, product_mrp_area): @@ -480,30 +489,35 @@ class MultiLevelMrp(models.TransientModel): for move in product_mrp_area.mrp_move_ids: if self._exclude_move(move): continue - if last_date and ( + if ( + last_date + and ( fields.Date.from_string(move.mrp_date) - >= last_date + timedelta(days=grouping_delta)) and ( - (onhand + last_qty + move.mrp_qty) - < product_mrp_area.mrp_minimum_stock - or (onhand + last_qty) - < product_mrp_area.mrp_minimum_stock): - name = 'Grouped Demand for %d Days' % grouping_delta - qtytoorder = product_mrp_area.mrp_minimum_stock - \ - onhand - last_qty + >= last_date + timedelta(days=grouping_delta) + ) + and ( + (onhand + last_qty + move.mrp_qty) + < product_mrp_area.mrp_minimum_stock + or (onhand + last_qty) < product_mrp_area.mrp_minimum_stock + ) + ): + name = "Grouped Demand for %d Days" % grouping_delta + qtytoorder = product_mrp_area.mrp_minimum_stock - onhand - last_qty cm = self.create_action( product_mrp_area_id=product_mrp_area, mrp_date=last_date, mrp_qty=qtytoorder, - name=name) - qty_ordered = cm.get('qty_ordered', 0.0) + name=name, + ) + qty_ordered = cm.get("qty_ordered", 0.0) onhand = onhand + last_qty + qty_ordered last_date = None last_qty = 0.00 nbr_create += 1 - if (onhand + last_qty + move.mrp_qty) < \ - product_mrp_area.mrp_minimum_stock or \ - (onhand + last_qty) < \ - product_mrp_area.mrp_minimum_stock: + if ( + (onhand + last_qty + move.mrp_qty) < product_mrp_area.mrp_minimum_stock + or (onhand + last_qty) < product_mrp_area.mrp_minimum_stock + ): if not last_date or last_qty == 0.0: last_date = fields.Date.from_string(move.mrp_date) last_qty = move.mrp_qty @@ -514,12 +528,15 @@ class MultiLevelMrp(models.TransientModel): onhand += move.mrp_qty if last_date and last_qty != 0.00: - name = 'Grouped Demand for %d Days' % grouping_delta + name = "Grouped Demand for %d Days" % grouping_delta qtytoorder = product_mrp_area.mrp_minimum_stock - onhand - last_qty cm = self.create_action( - product_mrp_area_id=product_mrp_area, mrp_date=last_date, - mrp_qty=qtytoorder, name=name) - qty_ordered = cm.get('qty_ordered', 0.0) + product_mrp_area_id=product_mrp_area, + mrp_date=last_date, + mrp_qty=qtytoorder, + name=name, + ) + qty_ordered = cm.get("qty_ordered", 0.0) onhand += qty_ordered nbr_create += 1 return nbr_create @@ -531,17 +548,17 @@ class MultiLevelMrp(models.TransientModel): @api.model def _mrp_calculation(self, mrp_lowest_llc, mrp_areas): - logger.info('Start MRP calculation') - product_mrp_area_obj = self.env['product.mrp.area'] + logger.info("Start MRP calculation") + product_mrp_area_obj = self.env["product.mrp.area"] counter = 0 if not mrp_areas: - mrp_areas = self.env['mrp.area'].search([]) + mrp_areas = self.env["mrp.area"].search([]) for mrp_area in mrp_areas: llc = 0 while mrp_lowest_llc > llc: product_mrp_areas = product_mrp_area_obj.search( - [('product_id.llc', '=', llc), - ('mrp_area_id', '=', mrp_area.id)]) + [("product_id.llc", "=", llc), ("mrp_area_id", "=", mrp_area.id)] + ) llc += 1 for product_mrp_area in product_mrp_areas: @@ -551,40 +568,47 @@ class MultiLevelMrp(models.TransientModel): for move in product_mrp_area.mrp_move_ids: if self._exclude_move(move): continue - qtytoorder = product_mrp_area.mrp_minimum_stock - \ - onhand - move.mrp_qty + qtytoorder = ( + product_mrp_area.mrp_minimum_stock + - onhand + - move.mrp_qty + ) if qtytoorder > 0.0: cm = self.create_action( product_mrp_area_id=product_mrp_area, mrp_date=move.mrp_date, - mrp_qty=qtytoorder, name=move.name) - qty_ordered = cm['qty_ordered'] + 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: nbr_create = self._init_mrp_move_grouped_demand( - nbr_create, product_mrp_area) + nbr_create, product_mrp_area + ) - if onhand < product_mrp_area.mrp_minimum_stock and \ - nbr_create == 0: - qtytoorder = \ - product_mrp_area.mrp_minimum_stock - onhand + if onhand < product_mrp_area.mrp_minimum_stock and nbr_create == 0: + qtytoorder = product_mrp_area.mrp_minimum_stock - onhand cm = self.create_action( product_mrp_area_id=product_mrp_area, mrp_date=date.today(), mrp_qty=qtytoorder, - name='Minimum Stock') - qty_ordered = cm['qty_ordered'] + name="Minimum Stock", + ) + qty_ordered = cm["qty_ordered"] onhand += qty_ordered counter += 1 - log_msg = 'MRP Calculation LLC %s Finished - Nbr. products: %s' % ( - llc - 1, counter) + log_msg = "MRP Calculation LLC {} Finished - Nbr. products: {}".format( + llc - 1, + counter, + ) logger.info(log_msg) - logger.info('Enb MRP calculation') + logger.info("Enb MRP calculation") @api.model def _get_demand_groups(self, product_mrp_area): @@ -595,9 +619,7 @@ class MultiLevelMrp(models.TransientModel): AND mrp_type = 'd' GROUP BY mrp_date """ - params = { - 'mrp_product': product_mrp_area.id - } + params = {"mrp_product": product_mrp_area.id} return query, params @api.model @@ -609,9 +631,7 @@ class MultiLevelMrp(models.TransientModel): AND mrp_type = 's' GROUP BY mrp_date """ - params = { - 'mrp_product': product_mrp_area.id, - } + params = {"mrp_product": product_mrp_area.id} return query, params @api.model @@ -622,15 +642,13 @@ class MultiLevelMrp(models.TransientModel): WHERE product_mrp_area_id = %(mrp_product)s GROUP BY due_date """ - params = { - 'mrp_product': product_mrp_area.id - } + params = {"mrp_product": product_mrp_area.id} return query, params @api.model def _init_mrp_inventory(self, product_mrp_area): - mrp_move_obj = self.env['mrp.move'] - planned_order_obj = self.env['mrp.planned.order'] + mrp_move_obj = self.env["mrp.move"] + planned_order_obj = self.env["mrp.planned.order"] # Read Demand demand_qty_by_date = {} query, params = self._get_demand_groups(product_mrp_area) @@ -650,59 +668,60 @@ class MultiLevelMrp(models.TransientModel): for mrp_date, qty in self.env.cr.fetchall(): planned_qty_by_date[mrp_date] = qty # Dates - moves_dates = mrp_move_obj.search([ - ('product_mrp_area_id', '=', product_mrp_area.id)], - order='mrp_date').mapped('mrp_date') - action_dates = planned_order_obj.search([ - ('product_mrp_area_id', '=', product_mrp_area.id)], - order='due_date').mapped('due_date') + moves_dates = mrp_move_obj.search( + [("product_mrp_area_id", "=", product_mrp_area.id)], order="mrp_date" + ).mapped("mrp_date") + action_dates = planned_order_obj.search( + [("product_mrp_area_id", "=", product_mrp_area.id)], order="due_date" + ).mapped("due_date") mrp_dates = set(moves_dates + action_dates) on_hand_qty = product_mrp_area.product_id.with_context( location=product_mrp_area.mrp_area_id.location_id.id - )._product_available()[ - product_mrp_area.product_id.id]['qty_available'] + )._product_available()[product_mrp_area.product_id.id]["qty_available"] running_availability = on_hand_qty for mdt in sorted(mrp_dates): mrp_inventory_data = { - 'product_mrp_area_id': product_mrp_area.id, - 'date': mdt, + "product_mrp_area_id": product_mrp_area.id, + "date": mdt, } demand_qty = demand_qty_by_date.get(mdt, 0.0) - mrp_inventory_data['demand_qty'] = abs(demand_qty) + mrp_inventory_data["demand_qty"] = abs(demand_qty) supply_qty = supply_qty_by_date.get(mdt, 0.0) - mrp_inventory_data['supply_qty'] = abs(supply_qty) - mrp_inventory_data['initial_on_hand_qty'] = on_hand_qty - on_hand_qty += (supply_qty + demand_qty) - mrp_inventory_data['final_on_hand_qty'] = on_hand_qty + mrp_inventory_data["supply_qty"] = abs(supply_qty) + mrp_inventory_data["initial_on_hand_qty"] = on_hand_qty + on_hand_qty += supply_qty + demand_qty + mrp_inventory_data["final_on_hand_qty"] = on_hand_qty # Consider that MRP plan is followed exactly: - running_availability += supply_qty \ - + demand_qty + planned_qty_by_date.get(mdt, 0.0) - mrp_inventory_data['running_availability'] = running_availability + running_availability += ( + supply_qty + demand_qty + planned_qty_by_date.get(mdt, 0.0) + ) + mrp_inventory_data["running_availability"] = running_availability - inv_id = self.env['mrp.inventory'].create(mrp_inventory_data) + inv_id = self.env["mrp.inventory"].create(mrp_inventory_data) # attach planned orders to inventory - planned_order_obj.search([ - ('due_date', '=', mdt), - ('product_mrp_area_id', '=', product_mrp_area.id), - ]).write( - {'mrp_inventory_id': inv_id.id}) + planned_order_obj.search( + [ + ("due_date", "=", mdt), + ("product_mrp_area_id", "=", product_mrp_area.id), + ] + ).write({"mrp_inventory_id": inv_id.id}) @api.model def _mrp_final_process(self, mrp_areas): - logger.info('Start MRP final process') - domain = [('product_id.llc', '<', 9999)] + logger.info("Start MRP final process") + domain = [("product_id.llc", "<", 9999)] if mrp_areas: - domain += [('mrp_area_id', 'in', mrp_areas.ids)] - product_mrp_area_ids = self.env['product.mrp.area'].search(domain) + domain += [("mrp_area_id", "in", mrp_areas.ids)] + product_mrp_area_ids = self.env["product.mrp.area"].search(domain) for product_mrp_area in product_mrp_area_ids: # Build the time-phased inventory if self._exclude_from_mrp( - product_mrp_area.product_id, - product_mrp_area.mrp_area_id): + product_mrp_area.product_id, product_mrp_area.mrp_area_id + ): continue self._init_mrp_inventory(product_mrp_area) - logger.info('End MRP final process') + logger.info("End MRP final process") @api.multi def run_mrp_multi_level(self):