[11.0][REN] multi_level_mrp -> mrp_multi_level

This commit is contained in:
Lois Rilo
2018-06-21 12:05:41 +02:00
committed by JasminSForgeFlow
parent 6abe68a4c4
commit 1a6b3493c6
34 changed files with 2791 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
===============
Multi Level MRP
===============
This module allows you to calculate, based in known inventory, demand, and
supply, and based on parameters set at product variant level, the new
procurements for each product.
To do this, the calculation starts at top level of the bill of material
and explodes this down to the lowest level.
Key Features
------------
* MRP parameters at product variant level
* Basic forecast system
* Cron job to calculate the MRP demand
* Manually calculate the MRP demand
* Confirm the calculated MRP demand and create BID's, PO's, or MO's
* View to see the products for which action is needed
Installation
============
Usage
=====
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/129/11.0
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/manufacture/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Original Odoo icons.
Contributors
------------
* Wim Audenaert <wim.audenaert@ucamco.com>
* Jordi Ballester <jordi.ballester@eficent.com>
* Lois Rilo <lois.rilo@eficent.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

View File

@@ -0,0 +1,2 @@
from . import models
from . import wizards

View File

@@ -0,0 +1,43 @@
# Copyright 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-18 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
'name': 'MRP Multi Level',
'version': '11.0.1.0.0',
'author': 'Ucamco, '
'Eficent, '
'Odoo Community Association (OCA)',
'summary': 'Adds an MRP Scheduler',
'website': 'https://github.com/OCA/manufacture',
'category': 'Manufacturing',
'depends': [
'mrp',
'stock',
'purchase',
'stock_demand_estimate',
],
'data': [
'security/mrp_multi_level_security.xml',
'security/ir.model.access.csv',
'views/mrp_area_view.xml',
'views/product_view.xml',
'views/stock_location_view.xml',
'views/mrp_product_view.xml',
'wizards/mrp_inventory_procure_view.xml',
'views/mrp_inventory_view.xml',
'wizards/mrp_multi_level_view.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/mrp_bom_demo.xml',
'demo/initial_on_hand_demo.xml',
],
'installable': True,
'application': True,
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mrp_area_stock_wh0" model="mrp.area">
<field name="name">WH/Stock</field>
<field name="warehouse_id" ref="stock.warehouse0"/>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
</odoo>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mrp_multi_level_cron" model="ir.cron">
<field name="name">Multi Level MRP</field>
<field name="model_id" ref="mrp_multi_level.model_mrp_multi_level"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model" eval="'mrp.multi.level'"/>
<field name="function" eval="'run_mrp_multi_level'" />
<field name="args" eval="'(None, )'"/>
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="stock_inventory_mrp_example" model="stock.inventory">
<field name="name">Simulating MRP</field>
</record>
<record id="stock_inventory_line_1" model="stock.inventory.line">
<field name="product_id" ref="product_product_pp_1"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_mrp_example"/>
<field name="product_qty">10</field>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<record id="stock_inventory_line_2" model="stock.inventory.line">
<field name="product_id" ref="product_product_pp_2"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_mrp_example"/>
<field name="product_qty">20</field>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<record id="stock_inventory_line_3" model="stock.inventory.line">
<field name="product_id" ref="product_product_sf_2"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="inventory_id" ref="stock_inventory_mrp_example"/>
<field name="product_qty">15</field>
<field name="location_id" ref="stock.stock_location_stock"/>
</record>
<function model="stock.inventory" name="action_done">
<function eval="[[('id', '=', ref('stock_inventory_mrp_example'))]]" model="stock.inventory" name="search"/>
</function>
</odoo>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- FP-1 -->
<record id="mrp_bom_fp_1" model="mrp.bom">
<field name="product_tmpl_id"
ref="product_product_fp_1_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
</record>
<record id="mrp_bom_fp_1_line_pp_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_1"/>
<field name="product_qty">2</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_1"/>
</record>
<record id="mrp_bom_fp_1_line_pp_2" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_2"/>
<field name="product_qty">3</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_1"/>
</record>
<!-- FP-2 -->
<record id="mrp_bom_fp_2" model="mrp.bom">
<field name="product_tmpl_id"
ref="product_product_fp_2_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
</record>
<record id="mrp_bom_fp_2_line_sf_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_sf_1"/>
<field name="product_qty">2</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_2"/>
</record>
<record id="mrp_bom_fp_2_line_sf_2" model="mrp.bom.line">
<field name="product_id" ref="product_product_sf_2"/>
<field name="product_qty">3</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_2"/>
</record>
<!-- SF-1 -->
<record id="mrp_bom_sf_1" model="mrp.bom">
<field name="product_tmpl_id"
ref="product_product_sf_1_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
</record>
<record id="mrp_bom_sf_1_line_pp_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_1"/>
<field name="product_qty">3</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_sf_1"/>
</record>
<record id="mrp_bom_sf_1_line_pp_2" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_2"/>
<field name="product_qty">2</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_sf_1"/>
</record>
<!-- SF-2 -->
<record id="mrp_bom_sf_2" model="mrp.bom">
<field name="product_tmpl_id"
ref="product_product_sf_2_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
</record>
<record id="mrp_bom_sf_2_line_pp_2" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_2"/>
<field name="product_qty">3</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_sf_2"/>
</record>
</odoo>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="product_category_mrp" model="product.category">
<field name="name">MRP</field>
</record>
</odoo>

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="product_product_fp_1" model="product.product">
<field name="name">FP-1</field>
<field name="categ_id" ref="product_category_mrp"/>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="produce_delay">2</field>
<field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/>
</record>
<record id="product_product_fp_2" model="product.product">
<field name="name">FP-2</field>
<field name="categ_id" ref="product_category_mrp"/>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="produce_delay">1</field>
<field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/>
</record>
<record id="product_product_sf_1" model="product.product">
<field name="name">SF-1</field>
<field name="categ_id" ref="product_category_mrp"/>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="produce_delay">1</field>
<field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/>
</record>
<record id="product_product_sf_2" model="product.product">
<field name="name">SF-2</field>
<field name="categ_id" ref="product_category_mrp"/>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="produce_delay">3</field>
<field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/>
</record>
<record id="product_product_pp_1" model="product.product">
<field name="name">PP-1</field>
<field name="categ_id" ref="product_category_mrp"/>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="route_ids" eval="[(6, 0, [ref('purchase.route_warehouse0_buy')])]"/>
</record>
<record id="product_product_pp_2" model="product.product">
<field name="name">PP-2</field>
<field name="categ_id" ref="product_category_mrp"/>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="route_ids" eval="[(6, 0, [ref('purchase.route_warehouse0_buy')])]"/>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="product_supplierinfo_pp_1" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_pp_1_product_template"/>
<field name="name" ref="res_partner_lazer_tech"/>
<field name="delay">4</field>
<field name="min_qty">0</field>
<field name="price">100</field>
</record>
<record id="product_supplierinfo_pp_2" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_pp_2_product_template"/>
<field name="name" ref="res_partner_lazer_tech"/>
<field name="delay">2</field>
<field name="min_qty">0</field>
<field name="price">100</field>
</record>
</odoo>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="res_partner_lazer_tech" model="res.partner">
<field name="name">Lazer Tech</field>
<field name="is_company">1</field>
<field name="company_id" ref="base.main_company"/>
<field name="customer" eval="False"/>
<field name="supplier" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,6 @@
from . import mrp_area
from . import stock_location
from . import product
from . import mrp_product
from . import mrp_move
from . import mrp_inventory

View File

@@ -0,0 +1,19 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# © 2016 Eficent Business and IT Consulting Services S.L.
# - Jordi Ballester Alomar <jordi.ballester@eficent.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class MrpArea(models.Model):
_name = 'mrp.area'
name = fields.Char('Name')
warehouse_id = fields.Many2one(
comodel_name='stock.warehouse', string='Warehouse',
required=True)
location_id = fields.Many2one(
comodel_name='stock.location', string='Location',
required=True)
active = fields.Boolean(default=True)

View File

@@ -0,0 +1,44 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-18 Eficent Business and IT Consulting Services S.L.
# - Jordi Ballester Alomar <jordi.ballester@eficent.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
class MrpInventory(models.Model):
_name = 'mrp.inventory'
_order = 'mrp_product_id, date'
_description = 'MRP inventory projections'
_rec_name = 'mrp_product_id'
# TODO: uom??
# 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
# TODO: substract qty already procured.
# TODO: show a LT based on the procure method?
# TODO: add to_procure_date
mrp_area_id = fields.Many2one(
comodel_name='mrp.area', string='MRP Area',
related='mrp_product_id.mrp_area_id',
)
mrp_product_id = fields.Many2one(
comodel_name='mrp.product', string='Product',
index=True,
)
uom_id = fields.Many2one(
comodel_name='product.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')
to_procure = fields.Float(string='To procure')
@api.multi
def _compute_uom_id(self):
for rec in self:
rec.uom_id = rec.mrp_product_id.product_id.uom_id

View File

@@ -0,0 +1,173 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# © 2016-18 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import models, fields, api, _
from odoo import exceptions
class MrpMove(models.Model):
_name = 'mrp.move'
_order = 'mrp_product_id, mrp_date, mrp_type desc, id'
# TODO: too many indexes...
mrp_area_id = fields.Many2one('mrp.area', 'MRP Area')
current_date = fields.Date('Current Date')
current_qty = fields.Float('Current Qty')
# TODO: remove purchase request and move to other module?
# TODO: cancel is not needed I think...
mrp_action = fields.Selection(
selection=[('mo', 'Manufacturing Order'),
('po', 'Purchase Order'),
('pr', 'Purchase Request'),
('so', 'Sale Order'),
('cancel', 'Cancel'),
('none', 'None')],
string='Action',
)
mrp_action_date = fields.Date('MRP Action Date')
mrp_date = fields.Date('MRP Date')
mrp_move_down_ids = fields.Many2many(
comodel_name='mrp.move',
relation='mrp_move_rel',
column1='move_up_id',
column2='move_down_id',
string='MRP Move DOWN',
)
mrp_move_up_ids = fields.Many2many(
comodel_name='mrp.move',
relation='mrp_move_rel',
column1='move_down_id',
column2='move_up_id',
string='MRP Move UP',
)
mrp_minimum_stock = fields.Float(
string='Minimum Stock',
related='product_id.mrp_minimum_stock',
)
mrp_order_number = fields.Char('Order Number')
# TODO: move purchase request to another module
mrp_origin = fields.Selection(
selection=[('mo', 'Manufacturing Order'),
('po', 'Purchase Order'),
('pr', 'Purchase Request'),
('so', 'Sale Order'),
('mv', 'Move'),
('fc', 'Forecast'), ('mrp', 'MRP')],
string='Origin')
mrp_processed = fields.Boolean('Processed')
mrp_product_id = fields.Many2one('mrp.product', 'Product', index=True)
mrp_qty = fields.Float('MRP Quantity')
mrp_type = fields.Selection(
selection=[('s', 'Supply'), ('d', 'Demand')],
string='Type',
)
name = fields.Char('Description')
parent_product_id = fields.Many2one(
comodel_name='product.product',
string='Parent Product', index=True,
)
product_id = fields.Many2one('product.product',
'Product', index=True)
production_id = fields.Many2one('mrp.production',
'Manufacturing Order', index=True)
purchase_line_id = fields.Many2one('purchase.order.line',
'Purchase Order Line', index=True)
purchase_order_id = fields.Many2one('purchase.order',
'Purchase Order', index=True)
running_availability = fields.Float('Running Availability')
sale_line_id = fields.Many2one('sale.order.line',
'Sale Order Line', index=True)
sale_order_id = fields.Many2one('sale.order', 'Sale 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',
)
stock_move_id = fields.Many2one('stock.move', 'Stock Move', index=True)
@api.model
def mrp_production_prepare(self, bom_id, routing_id):
return {
'product_uos_qty': 0.00,
'product_uom': self.product_id.product_tmpl_id.uom_id.id,
'product_qty': self.mrp_qty,
'product_id': self.product_id.id,
'location_src_id': 12,
'date_planned': self.mrp_date,
'cycle_total': 0.00,
'company_id': 1,
'state': 'draft',
'hour_total': 0.00,
'bom_id': bom_id,
'routing_id': routing_id,
'allow_reorder': False
}
@api.model
def mrp_process_mo(self):
if self.mrp_action != 'mo':
return True
bom_id = False
routing_id = False
mrp_boms = self.env['mrp.bom'].search(
[('product_id', '=', self.product_id.id),
('type', '=', 'normal')], limit=1)
for mrp_bom in mrp_boms:
bom_id = mrp_bom.id
routing_id = mrp_bom.routing_id.id
if self.product_id.track_production and self.mrp_qty > 1:
raise exceptions.Warning(_('Not allowed to create '
'manufacturing order with '
'quantity higher than 1 '
'for serialized product'))
else:
production_data = self.mrp_production_prepare(bom_id, routing_id)
pr = self.env['mrp.production'].create(production_data)
self.production_id = pr.id
self.current_qty = self.mrp_qty
self.current_date = self.mrp_date
self.mrp_processed = True
self.name = pr.name
# TODO: extension to purchase requisition in other module?
@api.model
def mrp_process_pr(self):
if self.mrp_action != 'pr':
return True
seq = self.env['ir.sequence'].search(
[('code', '=', 'purchase.order.requisition')])
seqnbr = self.env['ir.sequence'].next_by_id(seq.id)
self.env['purchase.requisition'].create({
'origin': 'MRP - [' + self.product_id.default_code + '] ' +
self.product_id.name,
'exclusive': 'exclusive',
'message_follower_ids': False,
'date_end': False,
'date_start': self.mrp_date,
'company_id': 1,
'warehouse_id': 1,
'state': 'draft',
'line_ids': [[0, False,
{'product_uom_id':
self.product_id.product_tmpl_id.uom_id.id,
'product_id': self.product_id.id,
'product_qty': self.mrp_qty,
'name': self.product_id.name}]],
'message_ids': False,
'description': False,
'name': seqnbr
})
self.current_qty = self.mrp_qty
self.current_date = self.mrp_date
self.mrp_processed = True
self.name = seqnbr

View File

@@ -0,0 +1,119 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# © 2016-18 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
class MrpProduct(models.Model):
_name = 'mrp.product'
mrp_area_id = fields.Many2one(
comodel_name='mrp.area', string='MRP Area',
)
current_qty_available = fields.Float(
string='Current Qty Available',
related='product_id.qty_available',
)
main_supplier_id = fields.Many2one(
comodel_name='res.partner', string='Main Supplier',
compute='_compute_main_supplier', store=True,
index=True,
)
mrp_inspection_delay = fields.Integer(
string='Inspection Delay', related='product_id.mrp_inspection_delay')
mrp_lead_time = fields.Float(
string='Lead Time',
related='product_id.produce_delay',
)
mrp_llc = fields.Integer(
string='Low Level Code', index=True, readonly=True,
)
# TODO: minimun stock and max/min order qty assigned by area?
mrp_maximum_order_qty = fields.Float(
string='Maximum Order Qty', related='product_id.mrp_maximum_order_qty',
)
mrp_minimum_order_qty = fields.Float(
string='Minimum Order Qty', related='product_id.mrp_minimum_order_qty',
)
mrp_minimum_stock = fields.Float(
string='Minimum Stock',
related='product_id.mrp_minimum_stock',
)
mrp_move_ids = fields.One2many(
comodel_name='mrp.move', inverse_name='mrp_product_id',
string='MRP Moves',
)
mrp_nbr_days = fields.Integer(
string='Nbr. Days', related='product_id.mrp_nbr_days')
mrp_qty_available = fields.Float('MRP Qty Available')
mrp_qty_multiple = fields.Float(string='Qty Multiple',
related='product_id.mrp_qty_multiple')
# TODO: this was: mrp_transit_delay = fields.Integer(mrp_move_ids) ??¿?¿¿?
mrp_transit_delay = fields.Integer(related='product_id.mrp_transit_delay')
mrp_verified = fields.Boolean(string='MRP Verified',
related='product_id.mrp_verified')
name = fields.Char('Description')
# TODO: rename to mrp_action_count?
nbr_mrp_actions = fields.Integer(
string='Nbr Actions', index=True,
)
nbr_mrp_actions_4w = fields.Integer(
string='Nbr Actions 4 Weeks', index=True,
)
product_id = fields.Many2one(
comodel_name='product.product', string='Product',
index=True,
)
product_tmpl_id = fields.Many2one('product.template', 'Product Template',
related='product_id.product_tmpl_id')
# TODO: extension to purchase requisition in other module?
# purchase_requisition = fields.Boolean(string='Purchase Requisition',
# related='product_id.purchase_requisition')
supply_method = fields.Selection(
selection=[('buy', 'Buy'),
('none', 'Undefined'),
('manufacture', 'Produce'),
('move', 'Transfer')],
string='Supply Method',
compute='_compute_supply_method', store=True,
)
@api.multi
@api.depends('mrp_area_id')
def _compute_supply_method(self):
group_obj = self.env['procurement.group']
for rec in self:
values = {
'warehouse_id': rec.mrp_area_id.warehouse_id,
'company_id': self.env.user.company_id.id, # TODO: better way to get company
}
rule = group_obj._get_rule(
rec.product_id, rec.mrp_area_id.location_id, values)
rec.supply_method = rule.action if rule else 'none'
@api.multi
@api.depends('supply_method')
def _compute_main_supplier(self):
"""Simplified and similar to procurement.rule logic."""
for rec in self.filtered(lambda r: r.supply_method == 'buy'):
suppliers = rec.product_id.seller_ids.filtered(
lambda r: (not r.product_id or r.product_id == rec.product_id))
if not suppliers:
continue
rec.main_supplier_id = suppliers[0].name
@api.multi
def _adjust_qty_to_order(self, qty_to_order):
# TODO: consider mrp_qty_multiple?
self.ensure_one()
if not self.mrp_maximum_order_qty and not self.mrp_minimum_order_qty:
return qty_to_order
if qty_to_order < self.mrp_minimum_order_qty:
return self.mrp_minimum_order_qty
if self.mrp_maximum_order_qty and qty_to_order > \
self.mrp_maximum_order_qty:
qty = self.mrp_maximum_order_qty
else:
qty = qty_to_order
return qty

View File

@@ -0,0 +1,47 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# © 2016 Eficent Business and IT Consulting Services S.L.
# - Jordi Ballester Alomar <jordi.ballester@eficent.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class Product(models.Model):
_inherit = 'product.product'
llc = fields.Integer('Low Level Code', default=0)
manufacturing_order_ids = fields.One2many(
comodel_name='mrp.production',
inverse_name='product_id',
string='Manufacturing Orders',
domain=[('state', '=', 'draft')],
)
mrp_applicable = fields.Boolean('MRP Applicable')
mrp_exclude = fields.Boolean('Exclude from MRP')
mrp_inspection_delay = fields.Integer('Inspection Delay', default=0)
mrp_maximum_order_qty = fields.Float(
string='Maximum Order Qty', default=0.0,
)
mrp_minimum_order_qty = fields.Float(
string='Minimum Order Qty', default=0.0,
)
mrp_minimum_stock = fields.Float('Minimum Stock')
mrp_nbr_days = fields.Integer(
string='Nbr. Days', default=0,
help="Number of days to group demand for this product during the "
"MRP run, in order to determine the quantity to order.",
)
mrp_product_ids = fields.One2many('mrp.product',
'product_id', 'MRP Product data')
mrp_qty_multiple = fields.Float('Qty Multiple', default=1.00)
mrp_transit_delay = fields.Integer('Transit Delay', default=0)
mrp_verified = fields.Boolean('Verified for MRP',
help="Identifies that this product has "
"been verified to be valid for the "
"MRP.")
purchase_order_line_ids = fields.One2many('purchase.order.line',
'product_id', 'Purchase Orders')
# TODO: extension to purchase requisition in other module?
# purchase_requisition_ids = fields.One2many('purchase.requisition.line',
# 'product_id',
# 'Purchase Requisitions')

View File

@@ -0,0 +1,16 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# © 2016 Eficent Business and IT Consulting Services S.L.
# - Jordi Ballester Alomar <jordi.ballester@eficent.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class StockLocation(models.Model):
_inherit = 'stock.location'
mrp_area_id = fields.Many2one(
comodel_name='mrp.area', string='MRP Area',
help="Requirements for a particular MRP area are combined for the "
"purposes of procurement by the MRP.",
)

View File

@@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mrp_inventory_user,mrp.inventory user,model_mrp_inventory,mrp.group_mrp_user,1,0,0,0
access_mrp_inventory_manager,mrp.inventory manager,model_mrp_inventory,mrp.group_mrp_manager,1,1,1,1
access_mrp_move_user,mrp.move user,model_mrp_move,mrp.group_mrp_user,1,0,0,0
access_mrp_move_manager,mrp.move manager,model_mrp_move,mrp.group_mrp_manager,1,1,1,1
access_mrp_product_user,mrp.product user,model_mrp_product,base.group_user,1,0,0,0
access_mrp_product_manager,mrp.product manager,model_mrp_product,mrp.group_mrp_manager,1,1,1,1
access_mrp_area_user,mrp.area user,model_mrp_area,mrp.group_mrp_user,1,0,0,0
access_mrp_area_manager,mrp.area manager,model_mrp_area,mrp.group_mrp_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mrp_inventory_user mrp.inventory user model_mrp_inventory mrp.group_mrp_user 1 0 0 0
3 access_mrp_inventory_manager mrp.inventory manager model_mrp_inventory mrp.group_mrp_manager 1 1 1 1
4 access_mrp_move_user mrp.move user model_mrp_move mrp.group_mrp_user 1 0 0 0
5 access_mrp_move_manager mrp.move manager model_mrp_move mrp.group_mrp_manager 1 1 1 1
6 access_mrp_product_user mrp.product user model_mrp_product base.group_user 1 0 0 0
7 access_mrp_product_manager mrp.product manager model_mrp_product mrp.group_mrp_manager 1 1 1 1
8 access_mrp_area_user mrp.area user model_mrp_area mrp.group_mrp_user 1 0 0 0
9 access_mrp_area_manager mrp.area manager model_mrp_area mrp.group_mrp_manager 1 1 1 1

View File

@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<odoo noupdate="1">
<record id="group_change_mrp_procure_qty" model="res.groups">
<field name="name">Change procure quantity in MRP</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,2 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_mrp_multi_level

View File

@@ -0,0 +1,366 @@
# Copyright 2018 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import date, datetime, timedelta
from odoo.tests.common import SavepointCase
from odoo import fields
from dateutil.rrule import WEEKLY
class TestMrpMultiLevel(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestMrpMultiLevel, cls).setUpClass()
cls.mo_obj = cls.env['mrp.production']
cls.po_obj = cls.env['purchase.order']
cls.product_obj = cls.env['product.product']
cls.partner_obj = cls.env['res.partner']
cls.stock_picking_obj = cls.env['stock.picking']
cls.estimate_obj = cls.env['stock.demand.estimate']
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_product_obj = cls.env['mrp.product']
cls.mrp_move_obj = cls.env['mrp.move']
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.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')
# Partner:
vendor1 = cls.partner_obj.create({'name': 'Vendor 1'})
# Create products:
route_buy = cls.env.ref('purchase.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})],
})
# Create test picking:
date_move = datetime.today() + timedelta(days=7)
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 PO:
date_po = datetime.today() + timedelta(days=1)
cls.po = cls.po_obj.create({
'name': 'Test PO-001',
'partner_id': cls.vendor.id,
'order_line': [
(0, 0, {
'name': 'Test PP-2 line',
'product_id': cls.pp_2.id,
'date_planned': date_po,
'product_qty': 5.0,
'product_uom': cls.pp_2.uom_id.id,
'price_unit': 25.0,
})],
})
# Create test MO:
date_mo = datetime.today() + timedelta(days=9)
bom_fp_2 = cls.env.ref('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 (Strings):
today = datetime.today()
cls.date_3 = fields.Date.to_string(today + timedelta(days=3))
cls.date_5 = fields.Date.to_string(today + timedelta(days=5))
cls.date_6 = fields.Date.to_string(today + timedelta(days=6))
cls.date_7 = fields.Date.to_string(today + timedelta(days=7))
cls.date_8 = fields.Date.to_string(today + timedelta(days=8))
cls.date_9 = fields.Date.to_string(today + timedelta(days=9))
cls.date_10 = fields.Date.to_string(today + timedelta(days=10))
# Create Date Ranges:
cls.dr_type = cls.env['date.range.type'].create({
'name': 'Weeks',
'company_id': False,
'allow_overlap': False,
})
generator = cls.env['date.range.generator'].create({
'date_start': today - timedelta(days=3),
'name_prefix': 'W-',
'type_id': cls.dr_type.id,
'duration_count': 1,
'unit_of_time': WEEKLY,
'count': 3})
generator.action_apply()
# Create Demand Estimates:
ranges = cls.env['date.range'].search(
[('type_id', '=', cls.dr_type.id)])
qty = 140.0
for dr in ranges:
qty += 70.0
cls._create_demand_estimate(
cls.prod_test, cls.stock_location, dr, qty)
cls.mrp_multi_level_wiz.create({}).run_mrp_multi_level()
@classmethod
def _create_demand_estimate(cls, product, location, date_range, qty):
cls.estimate_obj.create({
'product_id': product.id,
'location_id': location.id,
'product_uom': product.uom_id.id,
'product_uom_qty': qty,
'date_range_id': date_range.id,
})
def test_01_mrp_levels(self):
"""Tests computation of MRP levels."""
self.assertEqual(self.fp_1.llc, 0)
self.assertEqual(self.fp_2.llc, 0)
self.assertEqual(self.sf_1.llc, 1)
self.assertEqual(self.sf_2.llc, 1)
self.assertEqual(self.pp_1.llc, 2)
self.assertEqual(self.pp_2.llc, 2)
def test_02_mrp_product(self):
"""Tests that mrp products are generated correctly."""
mrp_product = self.mrp_product_obj.search([
('product_id', '=', self.pp_1.id)])
self.assertEqual(mrp_product.supply_method, 'buy')
self.assertEqual(mrp_product.main_supplier_id, self.vendor)
self.assertEqual(mrp_product.mrp_qty_available, 10.0)
mrp_product = self.mrp_product_obj.search([
('product_id', '=', self.sf_1.id)])
self.assertEqual(mrp_product.supply_method, 'manufacture')
def test_03_mrp_moves(self):
"""Tests for mrp moves generated."""
moves = self.mrp_move_obj.search([
('product_id', '=', self.pp_1.id),
('mrp_action', '=', 'none'),
])
self.assertEqual(len(moves), 3)
self.assertNotIn('s', moves.mapped('mrp_type'))
for move in moves:
self.assertTrue(move.mrp_move_up_ids)
if move.mrp_move_up_ids.mrp_product_id.product_id == self.fp_1:
# Demand coming from FP-1
self.assertEqual(move.mrp_move_up_ids.mrp_action, 'mo')
self.assertEqual(move.mrp_qty, -200.0)
elif move.mrp_move_up_ids.mrp_product_id.product_id == self.sf_1:
# Demand coming from FP-2 -> SF-1
self.assertEqual(move.mrp_move_up_ids.mrp_action, 'mo')
if move.mrp_date == self.date_5:
self.assertEqual(move.mrp_qty, -90.0)
elif move.mrp_date == self.date_8:
self.assertEqual(move.mrp_qty, -72.0)
# Check actions:
moves = self.mrp_move_obj.search([
('product_id', '=', self.pp_1.id),
('mrp_action', '!=', 'none'),
])
self.assertEqual(len(moves), 3)
for move in moves:
self.assertEqual(move.mrp_action, 'po')
self.assertEqual(move.mrp_type, 's')
# Check PP-2 PO being accounted:
po_move = self.mrp_move_obj.search([
('product_id', '=', self.pp_2.id),
('mrp_action', '=', 'none'),
('mrp_type', '=', 's'),
])
self.assertEqual(len(po_move), 1)
self.assertEqual(po_move.purchase_order_id, self.po)
self.assertEqual(po_move.purchase_line_id, self.po.order_line)
def test_04_mrp_multi_level(self):
"""Tests MRP inventories created."""
# FP-1
fp_1_inventory_lines = self.mrp_inventory_obj.search(
[('mrp_product_id.product_id', '=', self.fp_1.id)])
self.assertEqual(len(fp_1_inventory_lines), 1)
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([
('mrp_product_id.product_id', '=', self.fp_2.id),
('date', '=', self.date_7)])
self.assertEqual(len(fp_2_line_1), 1)
self.assertEqual(fp_2_line_1.demand_qty, 15.0)
self.assertEqual(fp_2_line_1.to_procure, 15.0)
# TODO: ask odoo to fix it... should be date10
fp_2_line_2 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.fp_2.id),
('date', '=', self.date_9)])
self.assertEqual(len(fp_2_line_2), 1)
self.assertEqual(fp_2_line_2.demand_qty, 0.0)
self.assertEqual(fp_2_line_2.to_procure, 0.0)
self.assertEqual(fp_2_line_2.supply_qty, 12.0)
# SF-1
sf_1_line_1 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.sf_1.id),
('date', '=', self.date_6)])
self.assertEqual(len(sf_1_line_1), 1)
self.assertEqual(sf_1_line_1.demand_qty, 30.0)
self.assertEqual(sf_1_line_1.to_procure, 30.0)
sf_1_line_2 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.sf_1.id),
('date', '=', self.date_9)])
self.assertEqual(len(sf_1_line_2), 1)
self.assertEqual(sf_1_line_2.demand_qty, 24.0)
self.assertEqual(sf_1_line_2.to_procure, 24.0)
# SF-2
sf_2_line_1 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.sf_2.id),
('date', '=', self.date_6)])
self.assertEqual(len(sf_2_line_1), 1)
self.assertEqual(sf_2_line_1.demand_qty, 45.0)
self.assertEqual(sf_2_line_1.to_procure, 30.0)
sf_2_line_2 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.sf_2.id),
('date', '=', self.date_9)])
self.assertEqual(len(sf_2_line_2), 1)
self.assertEqual(sf_2_line_2.demand_qty, 36.0)
self.assertEqual(sf_2_line_2.to_procure, 36.0)
# PP-1
pp_1_line_1 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.pp_1.id),
('date', '=', self.date_5)])
self.assertEqual(len(pp_1_line_1), 1)
self.assertEqual(pp_1_line_1.demand_qty, 290.0)
self.assertEqual(pp_1_line_1.to_procure, 280.0)
pp_1_line_2 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.pp_1.id),
('date', '=', self.date_8)])
self.assertEqual(len(pp_1_line_2), 1)
self.assertEqual(pp_1_line_2.demand_qty, 72.0)
self.assertEqual(pp_1_line_2.to_procure, 72.0)
# PP-2
pp_2_line_1 = self.mrp_inventory_obj.search([
('mrp_product_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([
('mrp_product_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([
('mrp_product_id.product_id', '=', self.pp_2.id),
('date', '=', self.date_6)])
self.assertEqual(len(pp_2_line_3), 1)
self.assertEqual(pp_2_line_3.demand_qty, 108.0)
self.assertEqual(pp_2_line_3.to_procure, 108.0)
pp_2_line_4 = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.pp_2.id),
('date', '=', self.date_8)])
self.assertEqual(len(pp_2_line_4), 1)
self.assertEqual(pp_2_line_4.demand_qty, 48.0)
self.assertEqual(pp_2_line_4.to_procure, 48.0)
def test_05_moves_extra_info(self):
"""Test running availability and actions counters computation on
mrp moves."""
# Running availability for PP-1:
moves = self.mrp_move_obj.search([
('product_id', '=', self.pp_1.id)],
order='mrp_date, mrp_type desc, id')
self.assertEqual(len(moves), 6)
expected = [200.0, 290.0, 90.0, 0.0, 72.0, 0.0]
self.assertEqual(moves.mapped('running_availability'), expected)
# Actions counters for PP-1:
mrp_product = self.mrp_product_obj.search([
('product_id', '=', self.pp_1.id)
])
self.assertEqual(mrp_product.nbr_mrp_actions, 3)
self.assertEqual(mrp_product.nbr_mrp_actions_4w, 3)
def test_06_demand_estimates(self):
"""Tests demand estimates integration."""
estimates = self.estimate_obj.search(
[('product_id', '=', self.prod_test.id)])
self.assertEqual(len(estimates), 3)
moves = self.mrp_move_obj.search([
('product_id', '=', self.prod_test.id),
])
# 3 weeks - 3 days in the past = 18 days of valid estimates:
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == 'd')
self.assertEqual(len(moves_from_estimates), 18)
quantities = moves_from_estimates.mapped('mrp_qty')
self.assertIn(-30.0, quantities) # 210 a week => 30.0 dayly:
self.assertIn(-40.0, quantities) # 280 a week => 40.0 dayly:
self.assertIn(-50.0, quantities) # 350 a week => 50.0 dayly:
actions = moves.filtered(lambda m: m.mrp_action == 'po')
self.assertEqual(len(actions), 18)
def test_07_procure_mo(self):
"""Test procurement wizard with MOs."""
mos = self.mo_obj.search([
('product_id', '=', self.fp_1.id)])
self.assertFalse(mos)
mrp_inv = self.mrp_inventory_obj.search([
('mrp_product_id.product_id', '=', self.fp_1.id)])
self.mrp_inventory_procure_wiz.with_context({
'active_model': 'mrp.inventory',
'active_ids': mrp_inv.ids,
'active_id': mrp_inv.id,
}).create({}).make_procurement()
mos = self.mo_obj.search([
('product_id', '=', self.fp_1.id)])
self.assertTrue(mos)
self.assertEqual(mos.product_qty, 100.0)
datetime_5 = fields.Datetime.to_string(
date.today() + timedelta(days=5))
self.assertEqual(mos.date_planned_start, datetime_5)
# TODO: test procure wizard: pos, multiple...
# TODO: test multiple destination IDS:...

View File

@@ -0,0 +1,60 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="mrp_area_tree">
<field name="name">mrp.area.tree</field>
<field name="model">mrp.area</field>
<field name="type">form</field>
<field name="arch" type="xml">
<tree string="MRP Area">
<field name="name"/>
<field name="warehouse_id"/>
<field name="location_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="mrp_area_form">
<field name="name">mrp.area.form</field>
<field name="model">mrp.area</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="MRP Area">
<group colspan="4" col="2">
<group>
<field name="name"/>
<field name="active"/>
</group>
<group>
<field name="warehouse_id"/>
<field name="location_id"/>
</group>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="mrp_area_action">
<field name="sequence" eval="20"/>
<field name="name">MRP Area</field>
<field name="res_model">mrp.area</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="mrp_area_tree"/>
<field name="act_window_id" ref="mrp_area_form"/>
</record>
<record model="ir.actions.act_window.view" id="mrp_area_form_action">
<field name="sequence" eval="22"/>
<field name="view_mode">form</field>
<field name="view_id" ref="mrp_area_form"/>
<field name="act_window_id" ref="mrp_area_action"/>
</record>
<record model="ir.actions.act_window.view" id="mrp_area_tree_action">
<field name="sequence" eval="20"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="mrp_area_tree"/>
<field name="act_window_id" ref="mrp_area_action"/>
</record>
</odoo>

View File

@@ -0,0 +1,97 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="mrp_inventory_form">
<field name="name">mrp.inventory.form</field>
<field name="model">mrp.inventory</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="MRP Area">
<group>
<field name="mrp_area_id"/>
<field name="mrp_product_id"/>
<field name="date"/>
<field name="initial_on_hand_qty"/>
<field name="demand_qty"/>
<field name="supply_qty"/>
<field name="final_on_hand_qty"/>
<field name="to_procure"/>
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="mrp_inventory_tree">
<field name="name">mrp.inventory.tree</field>
<field name="model">mrp.inventory</field>
<field name="type">form</field>
<field name="arch" type="xml">
<tree string="MRP Inventory">
<field name="mrp_area_id"/>
<field name="mrp_product_id"/>
<field name="date"/>
<field name="initial_on_hand_qty"/>
<field name="demand_qty"/>
<field name="supply_qty"/>
<field name="final_on_hand_qty"/>
<field name="to_procure"/>
<!--TODO: use same procurement wizard as ddmrp?-->
<button string="Create Procurement"
name="%(mrp_multi_level.act_mrp_inventory_procure)d"
icon="fa-cogs" type="action"
attrs="{'invisible':[('to_procure','==',0.0)]}"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="mrp_inventory_search">
<field name="name">mrp.inventory.search</field>
<field name="model">mrp.inventory</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="MRP Inventory">
<group name="select" expand="0" string="Selection...">
<field name="mrp_product_id" select='1'/>
<field name="mrp_area_id" select='1'/>
</group>
<separator/>
<filter string="To Procure" name="filter_to_procure"
domain="[['to_procure','>',0.0]]"/>
<separator/>
<group expand="0" string="Group By...">
<filter string="Product"
context="{'group_by':'mrp_product_id'}"/>
<filter string="MRP Area"
context="{'group_by':'mrp_area_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="mrp_inventory_action">
<field name="sequence" eval="20"/>
<field name="name">MRP Inventory</field>
<field name="res_model">mrp.inventory</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="mrp_inventory_tree"/>
<field name="search_view_id" ref="mrp_inventory_search"/>
<field name="act_window_id" ref="mrp_inventory_form"/>
</record>
<!--TODO: remove?-->
<record model="ir.actions.act_window.view" id="mrp_inventory_form_action">
<field name="sequence" eval="22"/>
<field name="view_mode">form</field>
<field name="view_id" ref="mrp_inventory_form"/>
<field name="act_window_id" ref="mrp_inventory_action"/>
</record>
<record model="ir.actions.act_window.view" id="mrp_inventory_tree_action">
<field name="sequence" eval="20"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="mrp_inventory_tree"/>
<field name="act_window_id" ref="mrp_inventory_action"/>
</record>
</odoo>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0"?>
<odoo>
<menuitem name="MRP"
id="menu_mrp_mrp"
parent="mrp.menu_mrp_root"
sequence="22"/>
<menuitem name="MRP Areas"
id="menu_mrp_areas"
action="mrp_area_action"
parent="menu_mrp_mrp"
sequence="10"/>
<menuitem name="MRP Products"
id="menu_mrp_products"
action="mrp_product_action"
parent="menu_mrp_mrp"
sequence="20"/>
<menuitem name="MRP Inventory"
id="menu_mrp_inventory"
action="mrp_inventory_action"
parent="menu_mrp_mrp"
sequence="30"/>
<menuitem name="Run Multi Level MRP"
id="menu_mrp_multi_level"
action="action_mrp_multi_level"
parent="menu_mrp_mrp"
sequence="40"/>
</odoo>

View File

@@ -0,0 +1,176 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="mrp_product_tree">
<field name="name">mrp.product.tree</field>
<field name="model">mrp.product</field>
<field name="type">form</field>
<field name="arch" type="xml">
<tree string="MRP Products">
<field name="mrp_area_id"/>
<field name="product_id"/>
<field name="mrp_verified"/>
<field name="mrp_move_ids"/>
<field name="nbr_mrp_actions_4w"/>
<field name="nbr_mrp_actions"/>
<field name="supply_method"/>
<field name="main_supplier_id"/>
<field name="mrp_llc" invisible="True"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="mrp_product_form">
<field name="name">mrp.product.form</field>
<field name="model">mrp.product</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="MRP Product">
<group colspan="4" col="2">
<group>
<field name="mrp_area_id" readonly="True"/>
<field name="product_id" readonly="True"/>
<field name="product_tmpl_id" readonly="True"/>
<field name="mrp_verified" readonly="True"/>
<field name="supply_method" readonly="True"/>
<!--<field name="purchase_requisition" readonly="True"/>-->
<field name="main_supplier_id" readonly="True"/>
<field name="mrp_lead_time" readonly="True"/>
<field name="mrp_nbr_days" readonly="True"/>
<field name="mrp_transit_delay" readonly="True"/>
<field name="mrp_inspection_delay" readonly="True"/>
</group>
<group>
<field name="mrp_llc" readonly="True"/>
<field name="mrp_qty_available" readonly="True"/>
<field name="current_qty_available" readonly="True"/>
<field name="mrp_minimum_stock" readonly="True"/>
<field name="mrp_minimum_order_qty" readonly="True"/>
<field name="mrp_maximum_order_qty" readonly="True"/>
<field name="mrp_qty_multiple" readonly="True"/>
</group>
</group>
<group colspan="2" col="2">
<separator string="Moves"/>
<field name="mrp_move_ids" nolabel="1" colspan="2" context="{'default_mrp_product_id': active_id}" readonly="True">
<tree string="Moves" colors="red:running_availability > mrp_minimum_stock">
<field name="mrp_area_id" invisible="True"/>
<field name="mrp_product_id" invisible="True"/>
<field name="mrp_action_date" readonly="True"/>
<field name="mrp_date" readonly="True"/>
<field name="current_date" readonly="True"/>
<field name="mrp_origin" readonly="True"/>
<field name="state" readonly="True"/>
<field name="mrp_order_number" readonly="True"/>
<field name="parent_product_id" readonly="True"/>
<field name="name" readonly="True"/>
<field name="mrp_qty" readonly="True"/>
<field name="current_qty" readonly="True"/>
<field name="running_availability" readonly="True"/>
<field name="mrp_minimum_stock" invisible="True"/>
<field name="mrp_action" readonly="True"/>
<field name="mrp_type" readonly="True"/>
<field name="mrp_move_up_ids" readonly="True"/>
<field name="mrp_processed" invisible="True"/>
</tree>
<form string="Moves">
<group colspan="4" col="2">
<group>
<field name="mrp_area_id"
invisible="True"/>
<field name="mrp_product_id" invisible="True"/>
<field name="mrp_qty" readonly="True"/>
<field name="current_qty" readonly="True"/>
<field name="mrp_date" readonly="True"/>
<field name="current_date" readonly="True"/>
<field name="mrp_action" readonly="True"/>
<field name="mrp_type" readonly="True"/>
</group>
<group>
<field name="production_id" readonly="True"/>
<field name="sale_order_id" readonly="True"/>
<field name="purchase_order_id" readonly="True"/>
<field name="stock_move_id" readonly="True"/>
<field name="mrp_processed" invisible="True"/>
</group>
</group>
<group colspan="2" col="2">
<field name="mrp_move_up_ids">
<tree string="Moves">
<field name="mrp_area_id"
invisible="True"/>
<field name="mrp_product_id" readonly="True"/>
<field name="mrp_date" readonly="True"/>
<field name="current_date" readonly="True"/>
<field name="mrp_origin" readonly="True"/>
<field name="state" readonly="True"/>
<field name="mrp_order_number" readonly="True"/>
<field name="parent_product_id" readonly="True"/>
<field name="name" readonly="True"/>
<field name="mrp_qty" readonly="True"/>
<field name="current_qty" readonly="True"/>
<field name="running_availability" readonly="True"/>
<field name="mrp_action" readonly="True"/>
<field name="mrp_type" readonly="True"/>
<field name="mrp_move_up_ids" readonly="True"/>
</tree>
</field>
</group>
</form>
</field>
</group>
</form>
</field>
</record>
<record model="ir.ui.view" id="mrp_search_form">
<field name="name">mrp.filter.form</field>
<field name="model">mrp.product</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Product">
<group expand="0" string="Selection...">
<field name="product_id" select='1'/>
<field name="mrp_area_id"/>
</group>
<separator/>
<filter string="With Moves" name="moves" domain="[['mrp_move_ids','!=',False]]"/>
<filter string="With Actions in Coming 4 Weeks" name="actions4w" domain="[['nbr_mrp_actions_4w','!=',0]]"/>
<filter string="With Actions" name="actions" domain="[['nbr_mrp_actions','!=',0]]"/>
<filter string="Purchase Actions" domain="[['supply_method','=','buy'],['nbr_mrp_actions','!=',0]]"/>
<filter string="Manufacture Actions" domain="[['supply_method','=','manufacture'],['nbr_mrp_actions','!=',0]]"/>
<separator/>
<group expand="0" string="Group By...">
<filter string="Supply Method" context="{'group_by':'supply_method'}"/>
<filter string="Main Supplier" context="{'group_by':'main_supplier_id'}"/>
<filter string="Low Level Code" context="{'group_by':'mrp_llc'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="mrp_product_action">
<field name="sequence" eval="20"/>
<field name="name">MRP Products</field>
<field name="res_model">mrp.product</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="mrp_product_tree"/>
<field name="search_view_id" ref="mrp_search_form"/>
<field name="act_window_id" ref="mrp_product_form"/>
</record>
<record model="ir.actions.act_window.view" id="mrp_product_form_action">
<field name="sequence" eval="22"/>
<field name="view_mode">form</field>
<field name="view_id" ref="mrp_product_form"/>
<field name="act_window_id" ref="mrp_product_action"/>
</record>
<record model="ir.actions.act_window.view" id="mrp_product_tree_action">
<field name="sequence" eval="20"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="mrp_product_tree"/>
<field name="act_window_id" ref="mrp_product_action"/>
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0"?>
<odoo>
<record model="ir.ui.view" id="view_mrp_product_product_form">
<field name="name">view.mrp.product.product.form</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="type">form</field>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="MRP">
<group colspan="4" col="2">
<group>
<field name="mrp_exclude"/>
<field name="mrp_verified"/>
<field name="mrp_nbr_days"/>
<field name="mrp_transit_delay"/>
<field name="mrp_inspection_delay"/>
</group>
<group>
<field name="mrp_minimum_stock"/>
<field name="mrp_minimum_order_qty"/>
<field name="mrp_maximum_order_qty"/>
<field name="mrp_qty_multiple"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0"?>
<odoo>
<record id="view_location_form" model="ir.ui.view">
<field name="name">stock.location.form</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_form"/>
<field name="arch" type="xml">
<field name="company_id" position="before">
<field name="mrp_area_id"/>
</field>
</field>
</record>
<record id="view_location_search" model="ir.ui.view">
<field name="name">stock.location.search</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_search"/>
<field name="arch" type="xml">
<field name="location_id" position="after">
<field name="mrp_area_id"/>
</field>
</field>
</record>
<record id="view_location_tree2" model="ir.ui.view">
<field name="name">stock.location.tree</field>
<field name="model">stock.location</field>
<field name="inherit_id" ref="stock.view_location_tree2"/>
<field name="arch" type="xml">
<field name="company_id" position="before">
<field name="mrp_area_id"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,2 @@
from . import mrp_multi_level
from . import mrp_inventory_procure

View File

@@ -0,0 +1,133 @@
# Copyright 2018 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
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'
item_ids = fields.One2many(
comodel_name='mrp.inventory.procure.item',
inverse_name='wiz_id',
string='Items',
)
@api.model
def _prepare_item(self, mrp_inventory):
return {
'qty': mrp_inventory.to_procure,
'uom_id': mrp_inventory.uom_id.id,
'date_planned': mrp_inventory.date,
'mrp_inventory_id': mrp_inventory.id,
'product_id': mrp_inventory.mrp_product_id.product_id.id,
'warehouse_id': mrp_inventory.mrp_area_id.warehouse_id.id,
'location_id': mrp_inventory.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"):
view_id = self.env.ref(
'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
return super(MrpInventoryProcure, self).fields_view_get(
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:
return res
assert active_model == 'mrp.inventory', 'Bad context propagation'
items = item_obj = self.env['mrp.inventory.procure.item']
for line in mrp_inventory_obj.browse(mrp_inventory_ids):
items += item_obj.create(self._prepare_item(line))
res['item_ids'] = [(6, 0, items.ids)]
return res
@api.multi
def make_procurement(self):
self.ensure_one()
errors = []
for item in self.item_ids:
if not item.qty:
raise ValidationError(_("Quantity must be positive."))
values = item._prepare_procurement_values()
# Run procurement
try:
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
)
except UserError as error:
errors.append(error.name)
if errors:
raise UserError('\n'.join(errors))
return {'type': 'ir.actions.act_window_close'}
class MrpInventoryProcureItem(models.TransientModel):
_name = 'mrp.inventory.procure.item'
wiz_id = fields.Many2one(
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='product.uom',
)
date_planned = fields.Date(string='Planned Date', required=False)
mrp_inventory_id = fields.Many2one(
string='Mrp Inventory',
comodel_name='mrp.inventory',
)
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': self.date_planned, # TODO: play with this...
'warehouse_id': self.warehouse_id,
# 'company_id': self.company_id, # TODO: consider company
'group_id': group,
}
@api.multi
@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)

View File

@@ -0,0 +1,59 @@
<?xml version="1.0"?>
<odoo>
<!-- Make Procurement with security access right -->
<record id="view_mrp_inventory_procure_wizard" model="ir.ui.view">
<field name="name">mrp.inventory.procure.form</field>
<field name="model">mrp.inventory.procure</field>
<field name="arch" type="xml">
<form string="Procurement Request">
<p class="oe_gray">
Use this assistant to procure for this product and date.
According to the product configuration,
this may trigger a draft purchase order, a manufacturing
order or a transfer picking.
</p>
<group name="items" string="Items">
<field name="item_ids" nolabel="1">
<tree string="Items" nocreate="1" editable="top">
<field name="mrp_inventory_id" invisible="True"/>
<field name="warehouse_id" groups="stock.group_stock_multi_locations" readonly="1"/>
<field name="location_id" groups="stock.group_stock_multi_locations" readonly="1"/>
<field name="product_id" readonly="1"/>
<field name="qty"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="date_planned"/>
</tree>
</field>
</group>
<footer>
<button string="Execute" name="make_procurement" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Make Procurement without security access right -->
<record id="view_mrp_inventory_procure_without_security" model="ir.ui.view">
<field name="name">mrp.inventory.procure.form - readonly qty</field>
<field name="model">mrp.inventory.procure</field>
<field name="inherit_id" ref="view_mrp_inventory_procure_wizard"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<field name="qty" position="attributes">
<attribute name="readonly">1</attribute>
</field>
</field>
</record>
<record id="act_mrp_inventory_procure" model="ir.actions.act_window">
<field name="name">Procure</field>
<field name="res_model">mrp.inventory.procure</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id"
ref="mrp_multi_level.model_mrp_inventory"/>
</record>
</odoo>

View File

@@ -0,0 +1,971 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-18 Eficent Business and IT Consulting Services S.L.
# - Jordi Ballester Alomar <jordi.ballester@eficent.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models, exceptions, _
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from datetime import date, datetime, timedelta
import logging
logger = logging.getLogger(__name__)
ODOO_READ_GROUP_DAY_FORMAT = '%d %b %Y'
class MultiLevelMrp(models.TransientModel):
_name = 'mrp.multi.level'
# TODO: dates are not being correctly computed for supply...
@api.model
def _prepare_mrp_product_data(self, product, mrp_area):
qty_available = 0.0
product_obj = self.env['product.product']
# TODO: move mrp_qty_available computation, maybe unreserved??
location_ids = self.env['stock.location'].search(
[('id', 'child_of', mrp_area.location_id.id)])
for location in location_ids:
product_l = product_obj.with_context(
{'location': location.id}).browse(product.id)
qty_available += product_l.qty_available
return {
'mrp_area_id': mrp_area.id,
'product_id': product.id,
'mrp_qty_available': product.qty_available,
'mrp_llc': product.llc,
'name': product.name,
}
@api.model
def _prepare_mrp_move_data_from_forecast(
self, estimate, mrp_product, date):
mrp_type = 'd'
origin = 'fc'
return {
'mrp_area_id': mrp_product.mrp_area_id.id,
'product_id': mrp_product.product_id.id,
'mrp_product_id': mrp_product.id,
'production_id': None,
'purchase_order_id': None,
'purchase_line_id': None,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': -estimate.daily_qty,
'current_qty': -estimate.daily_qty,
'mrp_date': date,
'current_date': date,
'mrp_action': 'none',
'mrp_type': mrp_type,
'mrp_processed': False,
'mrp_origin': origin,
'mrp_order_number': None,
'parent_product_id': None,
'running_availability': 0.00,
'name': 'Forecast',
'state': 'confirmed',
}
@api.model
def _prepare_mrp_move_data_from_stock_move(
self, mrp_product, move, direction='in'):
# TODO: Clean up to reduce dependencies
if not((move.location_id.usage == 'internal' and
move.location_dest_id.usage != 'internal') \
or (move.location_id.usage != 'internal' and
move.location_dest_id.usage == 'internal')):
# TODO: not sure about this...
return {}
if direction == 'out':
mrp_type = 'd'
product_qty = -move.product_qty
else:
mrp_type = 's'
product_qty = move.product_qty
po = po_line = so = so_line = None
mo = origin = order_number = parent_product_id = None
if move.purchase_line_id:
order_number = move.purchase_line_id.order_id.name
origin = 'po'
po = move.purchase_line_id.order_id.id
po_line = move.purchase_line_id.id
if move.production_id:
order_number = move.production_id.name
origin = 'mo'
mo = move.production_id.id
else:
# TODO: move.move_dest_id -> move.move_dest_ids. DONE, review
if move.move_dest_ids:
# move_dest_id = move.move_dest_ids[:1]
for move_dest_id in move.move_dest_ids:
if move_dest_id.production_id:
order_number = move_dest_id.production_id.name
origin = 'mo'
mo = move_dest_id.production_id.id
if move_dest_id.production_id.product_id:
parent_product_id = \
move_dest_id.production_id.product_id.id
else:
parent_product_id = move_dest_id.product_id.id
if order_number is None:
order_number = move.name
mrp_date = date.today()
if datetime.date(datetime.strptime(
move.date_expected,
DEFAULT_SERVER_DATETIME_FORMAT)) > date.today():
mrp_date = datetime.date(datetime.strptime(
move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT))
return {
'mrp_area_id': mrp_product.mrp_area_id.id,
'product_id': move.product_id.id,
'mrp_product_id': mrp_product.id,
'production_id': mo,
'purchase_order_id': po,
'purchase_line_id': po_line,
'sale_order_id': so,
'sale_line_id': so_line,
'stock_move_id': move.id,
'mrp_qty': product_qty,
'current_qty': product_qty,
'mrp_date': mrp_date,
'current_date': move.date_expected,
'mrp_action': 'none',
'mrp_type': mrp_type,
'mrp_processed': False,
'mrp_origin': origin,
'mrp_order_number': order_number,
'parent_product_id': parent_product_id,
'running_availability': 0.00,
'name': order_number,
'state': move.state,
}
@api.model
def _prepare_mrp_move_data_supply(
self, mrp_product, qty, mrp_date_supply, mrp_action_date,
mrp_action, name):
return {
'mrp_area_id': mrp_product.mrp_area_id.id,
'product_id': mrp_product.product_id.id,
'mrp_product_id': mrp_product.id,
'production_id': None,
'purchase_order_id': None,
'purchase_line_id': None,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': qty,
'current_qty': None,
'mrp_date': mrp_date_supply,
'mrp_action_date': mrp_action_date,
'current_date': None,
'mrp_action': mrp_action,
'mrp_type': 's',
'mrp_processed': False,
'mrp_origin': None,
'mrp_order_number': None,
'parent_product_id': None,
'name': 'Supply: ' + name,
}
@api.model
def _prepare_mrp_move_data_bom_explosion(
self, product, bomline, qty, mrp_date_demand_2, bom, name):
mrp_product = self._get_mrp_product_from_product_and_area(
bomline.product_id, product.mrp_area_id)
if not mrp_product:
raise exceptions.Warning(
_("No MRP product found"))
return {
'mrp_area_id': product.mrp_area_id.id,
'product_id': bomline.product_id.id,
'mrp_product_id': mrp_product.id,
'production_id': None,
'purchase_order_id': None,
'purchase_line_id': None,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': -(qty * bomline.product_qty), # TODO: review with UoM
'current_qty': None,
'mrp_date': mrp_date_demand_2,
'current_date': None,
'mrp_action': 'none',
'mrp_type': 'd',
'mrp_processed': False,
'mrp_origin': 'mrp',
'mrp_order_number': None,
'parent_product_id': bom.product_id.id,
'name':
('Demand Bom Explosion: ' + name).replace(
'Demand Bom Explosion: Demand Bom '
'Explosion: ',
'Demand Bom Explosion: '),
}
@api.model
def create_move(self, mrp_product_id, mrp_date, mrp_qty, name):
self = self.with_context(auditlog_disabled=True)
values = {}
if not isinstance(mrp_date, date):
mrp_date = fields.Date.from_string(mrp_date)
if mrp_product_id.supply_method == 'buy':
# if mrp_product_id.purchase_requisition:
# mrp_action = 'pr'
# else:
mrp_action = 'po'
else:
# TODO: consider 'none'...
mrp_action = 'mo'
if mrp_date < date.today():
mrp_date_supply = date.today()
else:
mrp_date_supply = mrp_date
mrp_action_date = mrp_date - timedelta(
days=mrp_product_id.mrp_lead_time)
qty_ordered = 0.00
qty_to_order = mrp_qty
while qty_ordered < mrp_qty:
qty = mrp_product_id._adjust_qty_to_order(qty_to_order)
qty_to_order -= qty
move_data = self._prepare_mrp_move_data_supply(
mrp_product_id, qty, mrp_date_supply, mrp_action_date,
mrp_action, name)
mrpmove_id = self.env['mrp.move'].create(move_data)
qty_ordered = qty_ordered + qty
if mrp_action == 'mo':
mrp_date_demand = mrp_date - timedelta(
days=mrp_product_id.mrp_lead_time)
if mrp_date_demand < date.today():
mrp_date_demand = date.today()
if not mrp_product_id.product_id.bom_ids:
continue
bomcount = 0
for bom in mrp_product_id.product_id.bom_ids:
if not bom.active or not bom.bom_line_ids:
continue
bomcount += 1
if bomcount != 1:
continue
for bomline in bom.bom_line_ids:
if bomline.product_qty <= 0.00:
continue
if self._exclude_from_mrp(
mrp_product_id.mrp_area_id, bomline.product_id):
# Stop explosion.
continue
mrp_date_demand_2 = mrp_date_demand - timedelta( # TODO: review this...
days=(mrp_product_id.mrp_transit_delay +
mrp_product_id.mrp_inspection_delay))
move_data = \
self._prepare_mrp_move_data_bom_explosion(
mrp_product_id, bomline, qty,
mrp_date_demand_2,
bom, name)
mrpmove_id2 = self.env['mrp.move'].create(move_data)
sql_stat = "INSERT INTO mrp_move_rel (" \
"move_up_id, " \
"move_down_id) values (%d, %d)" % \
(mrpmove_id, mrpmove_id2, )
self.env.cr.execute(sql_stat)
values['qty_ordered'] = qty_ordered
log_msg = '%s' % qty_ordered
logger.info(log_msg)
return values
@api.model
def _mrp_cleanup(self):
# Some part of the code with the new API is replaced by
# sql statements due to performance issues when the auditlog is
# installed
logger.info('START MRP CLEANUP')
self.env['mrp.move'].search([]).unlink()
self.env['mrp.product'].search([]).unlink()
self.env['mrp.inventory'].search([]).unlink()
logger.info('END MRP CLEANUP')
return True
@api.model
def _low_level_code_calculation(self):
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)])
if products:
counter = len(products)
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)])
counter = len(products)
log_msg = 'LOW LEVEL CODE %s FINISHED - NBR PRODUCTS: %s' % (
llc, counter)
logger.info(log_msg)
mrp_lowest_llc = llc
logger.info('END LOW LEVEL CODE CALCULATION')
return mrp_lowest_llc
@api.model
def _calculate_mrp_applicable(self):
# TODO: Refactor all code here
self.env['product.product'].search([
('type', '=', 'product'),
]).write({'mrp_applicable': True})
return True
logger.info('CALCULATE MRP APPLICABLE')
sql_stat = '''UPDATE product_product SET mrp_applicable = False;'''
self.env.cr.execute(sql_stat)
sql_stat = """
UPDATE product_product
SET mrp_applicable=True
FROM product_template
WHERE product_tmpl_id = product_template.id
AND product_template.active = True
AND product_template.type = 'product'
AND mrp_minimum_stock > (
SELECT sum(quantity) FROM stock_quant, stock_location
WHERE stock_quant.product_id = product_product.id
AND stock_quant.location_id = stock_location.id
AND stock_location.usage = 'internal');"""
self.env.cr.execute(sql_stat)
sql_stat = """
UPDATE product_product
SET mrp_applicable=True
FROM product_template
WHERE product_tmpl_id = product_template.id
AND product_template.active = True
AND product_template.type = 'product'
AND product_product.id in (
SELECT distinct product_id
FROM stock_move
WHERE state <> 'draft' AND state <> 'cancel');"""
self.env.cr.execute(sql_stat)
sql_stat = """
UPDATE product_product
SET mrp_applicable=True
FROM product_template
WHERE product_tmpl_id = product_template.id
AND product_template.active = True
AND product_template.type = 'product'
AND llc > 0;"""
self.env.cr.execute(sql_stat)
# self.env.cr.commit()
counter = 0
sql_stat = """
SELECT count(id) AS counter
FROM product_product
WHERE mrp_applicable = True"""
self.env.cr.execute(sql_stat)
sql_res = self.env.cr.dictfetchone()
if sql_res:
counter = sql_res['counter']
log_msg = 'END CALCULATE MRP APPLICABLE: %s' % counter
logger.info(log_msg)
@api.model
def _init_mrp_product(self, product, mrp_area):
mrp_product_data = self._prepare_mrp_product_data(
product, mrp_area)
return self.env['mrp.product'].create(mrp_product_data)
@api.model
def _init_mrp_move_from_forecast(self, mrp_product):
locations = self.env['stock.location'].search(
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
today = fields.Date.today()
estimates = self.env['stock.demand.estimate'].search([
('product_id', '=', mrp_product.product_id.id),
('location_id', 'in', locations.ids),
('date_range_id.date_end', '>=', today)
])
for rec in estimates:
start = rec.date_range_id.date_start
if start < today:
start = today
mrp_date = fields.Date.from_string(start)
date_end = fields.Date.from_string(rec.date_range_id.date_end)
delta = timedelta(days=1)
while mrp_date <= date_end:
mrp_move_data = \
self._prepare_mrp_move_data_from_forecast(
rec, mrp_product, mrp_date)
self.env['mrp.move'].create(mrp_move_data)
mrp_date += delta
return True
# TODO: move this methods to mrp_product?? to be able to show moves with an action
@api.model
def _in_stock_moves_domain(self, mrp_product):
locations = self.env['stock.location'].search(
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
return [
('product_id', '=', mrp_product.product_id.id),
('state', 'not in', ['done', 'cancel']),
('product_qty', '>', 0.00),
('location_id', 'not in', locations.ids),
('location_dest_id', 'in', locations.ids),
]
@api.model
def _out_stock_moves_domain(self, mrp_product):
locations = self.env['stock.location'].search(
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
return [
('product_id', '=', mrp_product.product_id.id),
('state', 'not in', ['done', 'cancel']),
('product_qty', '>', 0.00),
('location_id', 'in', locations.ids),
('location_dest_id', 'not in', locations.ids),
]
@api.model
def _init_mrp_move_from_stock_move(self, mrp_product):
# TODO: Should we exclude the quantity done from the moves?
move_obj = self.env['stock.move']
mrp_move_obj = self.env['mrp.move']
in_domain = self._in_stock_moves_domain(mrp_product)
in_moves = move_obj.search(in_domain)
out_domain = self._out_stock_moves_domain(mrp_product)
out_moves = move_obj.search(out_domain)
if in_moves:
for move in in_moves:
move_data = self._prepare_mrp_move_data_from_stock_move(
mrp_product, move, direction='in')
mrp_move_obj.create(move_data)
if out_moves:
for move in out_moves:
move_data = self._prepare_mrp_move_data_from_stock_move(
mrp_product, move, direction='out')
mrp_move_obj.create(move_data)
return True
# TODO: extension to purchase requisition in other module?
@api.model
def _prepare_mrp_move_data_from_purchase_requisition(self, preql,
mrp_product):
mrp_date = date.today()
if preql.requisition_id.schedule_date and \
datetime.date(datetime.strptime(
preql.requisition_id.schedule_date,
'%Y-%m-%d %H:%M:%S')) > date.today():
mrp_date = datetime.date(datetime.strptime(
preql.requisition_id.schedule_date,
'%Y-%m-%d %H:%M:%S'))
return {
'product_id': preql.product_id.id,
'mrp_product_id': mrp_product.id,
'production_id': None,
'purchase_order_id': None,
'purchase_line_id': None,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': preql.product_qty,
'current_qty': preql.product_qty,
'mrp_date': mrp_date,
'current_date': preql.requisition_id.schedule_date,
'mrp_action': 'none',
'mrp_type': 's',
'mrp_processed': False,
'mrp_origin': 'pr',
'mrp_order_number': preql.requisition_id.name,
'parent_product_id': None,
'running_availability': 0.00,
'name': preql.requisition_id.name,
'state': preql.requisition_id.state,
}
# TODO: extension to purchase requisition in other module?
@api.model
def _init_mrp_move_from_purchase_requisition(self, mrp_product):
location_ids = self.env['stock.location'].search(
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
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]
requisitions = self.env['purchase.requisition'].search(
[('picking_type_id', 'in', picking_type_ids),
('state', '=', 'draft')])
req_lines = self.env['purchase.requisition.line'].search(
[('requisition_id', 'in', requisitions.ids),
('product_qty', '>', 0.0),
('product_id', '=', mrp_product.product_id.id)])
for preql in req_lines:
mrp_move_data = \
self._prepare_mrp_move_data_from_purchase_requisition(
preql, mrp_product)
self.env['mrp.move'].create(mrp_move_data)
@api.model
def _prepare_mrp_move_data_from_purchase_order(self, poline, mrp_product):
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,
'mrp_product_id': mrp_product.id,
'production_id': None,
'purchase_order_id': poline.order_id.id,
'purchase_line_id': poline.id,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': poline.product_qty,
'current_qty': poline.product_qty,
'mrp_date': mrp_date,
'current_date': poline.date_planned,
'mrp_action': 'none',
'mrp_type': 's',
'mrp_processed': False,
'mrp_origin': 'po',
'mrp_order_number': poline.order_id.name,
'parent_product_id': None,
'running_availability': 0.00,
'name': poline.order_id.name,
'state': poline.order_id.state,
}
@api.model
def _init_mrp_move_from_purchase_order(self, mrp_product):
location_ids = self.env['stock.location'].search(
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
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', '=', mrp_product.product_id.id)])
for line in po_lines:
mrp_move_data = \
self._prepare_mrp_move_data_from_purchase_order(
line, mrp_product)
self.env['mrp.move'].create(mrp_move_data)
@api.model
def _prepare_mrp_move_data_from_mrp_production(self, mo, mrp_product):
mrp_date = date.today()
if datetime.date(datetime.strptime(
mo.date_planned, '%Y-%m-%d %H:%M:%S')) > date.today():
mrp_date = datetime.date(datetime.strptime(
mo.date_planned, '%Y-%m-%d %H:%M:%S'))
return {
'mrp_area_id': mrp_product.mrp_area_id.id,
'product_id': mo.product_id.id,
'mrp_product_id': mrp_product.id,
'production_id': mo.id,
'purchase_order_id': None,
'purchase_line_id': None,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': mo.product_qty,
'current_qty': mo.product_qty,
'mrp_date': mrp_date,
'current_date': mo.date_planned,
'mrp_action': 'none',
'mrp_type': 's',
'mrp_processed': False,
'mrp_origin': 'mo',
'mrp_order_number': mo.name,
'parent_product_id': None,
'running_availability': 0.00,
'name': mo.name,
'state': mo.state,
}
@api.model
def _get_mrp_product_from_product_and_area(self, product, mrp_area):
return self.env['mrp.product'].search([
('product_id', '=', product.id),
('mrp_area_id', '=', mrp_area.id),
], limit=1)
@api.model
def _prepare_mrp_move_data_from_mrp_production_bom(
self, mo, bomline, mrp_date_demand, mrp_product):
line_mrp_product = self._get_mrp_product_from_product_and_area(
bomline.product_id, mrp_product.mrp_area_id)
return {
'mrp_area_id': mrp_product.mrp_area_id.id,
'product_id': bomline.product_id.id,
'mrp_product_id': line_mrp_product.id,
'production_id': mo.id,
'purchase_order_id': None,
'purchase_line_id': None,
'sale_order_id': None,
'sale_line_id': None,
'stock_move_id': None,
'mrp_qty': -(mo.product_qty * bomline.product_qty),
'current_qty': None,
'mrp_date': mrp_date_demand,
'current_date': None,
'mrp_action': 'none',
'mrp_type': 'd',
'mrp_processed': False,
'mrp_origin': 'mo',
'mrp_order_number': mo.name,
'parent_product_id': mo.product_id.id,
'name': ('Demand Bom Explosion: ' + mo.name),
}
@api.model
def _init_mrp_move_from_mrp_production_bom(self, mo, mrp_product):
# TODO: iniciate this from moves with MOs and for other moves from bom lines??
# TODO: fix this...
# mrp_date = date.today()
# mrp_date_demand = mrp_date - timedelta(
# days=mrp_product.product_id.produce_delay)
# if mrp_date_demand < date.today():
# mrp_date_demand = date.today()
mrp_date_demand = date.today()
if mo.bom_id and mo.bom_id.bom_line_ids:
for bomline in mo.bom_id.bom_line_ids:
if bomline.product_qty <= 0.00:
continue
# TODO: ['mrp.bom.line'].date_start does not exist in v11. Remove:
# if (bomline.date_start and datetime.date(
# datetime.strptime(bomline.date_start, '%Y-%m-%d')) >=
# mrp_date_demand):
# continue
# if (bomline.date_stop and datetime.date(
# datetime.strptime(bomline.date_stop, '%Y-%m-%d')) <=
# mrp_date_demand):
# continue
# TODO: add conditions to do this: ddmrp, not already existing MOs (MTO)...
mrp_move_data = \
self._prepare_mrp_move_data_from_mrp_production_bom(
mo, bomline, mrp_date_demand, mrp_product)
self.env['mrp.move'].create(mrp_move_data)
@api.model
def _init_mrp_move_from_mrp_production(self, mrp_product):
location_ids = self.env['stock.location'].search(
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
# TODO: there is no 'draft' state anymore. there will always be stock.moves
production_orders = self.env['mrp.production'].search([
('product_id', '=', mrp_product.product_id.id),
('location_dest_id', 'in', location_ids.ids),
('product_qty', '>', 0.0),
('state', 'in', ['confirmed', 'planned']),
]) # TODO: 'progress' as well?
for mo in production_orders:
# mrp_move_data = \
# self._prepare_mrp_move_data_from_mrp_production(
# mo, mrp_product)
# self.env['mrp.move'].create(mrp_move_data)
self._init_mrp_move_from_mrp_production_bom(mo, mrp_product)
@api.model
def _init_mrp_move(self, mrp_product):
self._init_mrp_move_from_forecast(mrp_product)
self._init_mrp_move_from_stock_move(mrp_product)
# TODO: extension to purchase requisition in other module?
# self._init_mrp_move_from_purchase_requisition(mrp_product)
self._init_mrp_move_from_purchase_order(mrp_product)
# TODO: not needed I think... check case when MO are partially done and posted...
# self._init_mrp_move_from_mrp_production(mrp_product)
@api.model
def _exclude_from_mrp(self, mrp_area, product):
""" To extend with various logic where needed. """
return product.mrp_exclude
@api.model
def _mrp_initialisation(self):
logger.info('START MRP INITIALISATION')
mrp_areas = self.env['mrp.area'].search([])
products = self.env['product.product'].search([
('mrp_applicable', '=', True)])
init_counter = 0
for mrp_area in mrp_areas:
for product in products:
if self._exclude_from_mrp(mrp_area, product):
continue
init_counter += 1
log_msg = 'MRP INIT: %s - %s ' % (
init_counter, product.default_code)
logger.info(log_msg)
mrp_product = self._init_mrp_product(product, mrp_area)
# for mrp_product in self.env['mrp.product'].search([]):
self._init_mrp_move(mrp_product)
logger.info('END MRP INITIALISATION')
@api.model
def _init_mrp_move_grouped_demand(self, nbr_create, mrp_product):
last_date = None
last_qty = 0.00
move_ids = []
for move in mrp_product.mrp_move_ids:
move_ids.append(move.id)
for move_id in move_ids:
move_rec = self.env['mrp.move'].search(
[('id', '=', move_id)])
for move in move_rec:
if move.mrp_action == 'none':
if last_date is not None:
if datetime.date(
datetime.strptime(
move.mrp_date, '%Y-%m-%d')) \
> last_date+timedelta(
days=mrp_product.mrp_nbr_days):
if (onhand + last_qty + move.mrp_qty) \
< mrp_product.mrp_minimum_stock \
or (onhand + last_qty) \
< mrp_product.mrp_minimum_stock:
name = 'Grouped Demand for ' \
'%d Days' % (
mrp_product.mrp_nbr_days, )
qtytoorder = \
mrp_product.mrp_minimum_stock - \
mrp_product - last_qty
cm = self.create_move(
mrp_product_id=mrp_product.id,
mrp_date=last_date,
mrp_qty=qtytoorder,
name=name)
qty_ordered = cm['qty_ordered']
onhand = onhand + last_qty + qty_ordered
last_date = None
last_qty = 0.00
nbr_create += 1
if (onhand + last_qty + move.mrp_qty) < \
mrp_product.mrp_minimum_stock or \
(onhand + last_qty) < \
mrp_product.mrp_minimum_stock:
if last_date is None:
last_date = datetime.date(
datetime.strptime(move.mrp_date,
'%Y-%m-%d'))
last_qty = move.mrp_qty
else:
last_qty = last_qty + move.mrp_qty
else:
last_date = datetime.date(
datetime.strptime(move.mrp_date,
'%Y-%m-%d'))
onhand = onhand + move.mrp_qty
if last_date is not None and last_qty != 0.00:
name = 'Grouped Demand for %d Days' % \
(mrp_product.mrp_nbr_days, )
qtytoorder = mrp_product.mrp_minimum_stock - onhand - last_qty
cm = self.create_move(
mrp_product_id=mrp_product.id, mrp_date=last_date,
mrp_qty=qtytoorder, name=name)
qty_ordered = cm['qty_ordered']
onhand += qty_ordered
nbr_create += 1
return nbr_create
@api.model
def _mrp_calculation(self, mrp_lowest_llc):
logger.info('START MRP CALCULATION')
mrp_product_obj = self.env['mrp.product']
counter = 0
for mrp_area in self.env['mrp.area'].search([]):
llc = 0
while mrp_lowest_llc > llc:
mrp_products = mrp_product_obj.search(
[('mrp_llc', '=', llc),
('mrp_area_id', '=', mrp_area.id)])
llc += 1
for mrp_product in mrp_products:
nbr_create = 0
onhand = mrp_product.mrp_qty_available # TODO: qty unreserved?
if mrp_product.mrp_nbr_days == 0:
# todo: review ordering by date
for move in mrp_product.mrp_move_ids:
if move.mrp_action == 'none':
if (onhand + move.mrp_qty) < \
mrp_product.mrp_minimum_stock:
qtytoorder = \
mrp_product.mrp_minimum_stock - \
onhand - move.mrp_qty
cm = self.create_move(
mrp_product_id=mrp_product,
mrp_date=move.mrp_date,
mrp_qty=qtytoorder, name=move.name)
qty_ordered = cm['qty_ordered']
onhand += move.mrp_qty + qty_ordered
nbr_create += 1
else:
onhand += move.mrp_qty
else:
# TODO: review this
nbr_create = self._init_mrp_move_grouped_demand(
nbr_create, mrp_product)
if onhand < mrp_product.mrp_minimum_stock and \
nbr_create == 0:
qtytoorder = mrp_product.mrp_minimum_stock - onhand
cm = self.create_move(
mrp_product_id=mrp_product,
mrp_date=date.today(),
mrp_qty=qtytoorder,
name='Minimum Stock')
qty_ordered = cm['qty_ordered']
onhand += qty_ordered
counter += 1
log_msg = 'MRP CALCULATION LLC %s FINISHED - NBR PRODUCTS: %s' % (
llc - 1, counter)
logger.info(log_msg)
logger.info('END MRP CALCULATION')
@api.model
def _init_mrp_inventory(self, mrp_product):
mrp_move_obj = self.env['mrp.move']
# Read Demand
demand_groups = mrp_move_obj.read_group(
[('mrp_product_id', '=', mrp_product.id),
('mrp_type', '=', 'd')],
['mrp_date', 'mrp_qty'], ['mrp_date:day'],
)
demand_qty_by_date = {}
for group in demand_groups:
# Reformat date back to default server format.
group_date = datetime.strptime(
group['mrp_date:day'], ODOO_READ_GROUP_DAY_FORMAT).strftime(
DEFAULT_SERVER_DATE_FORMAT)
demand_qty_by_date[group_date] = group['mrp_qty']
# Read Supply
supply_groups = mrp_move_obj.read_group(
[('mrp_product_id', '=', mrp_product.id),
('mrp_type', '=', 's'),
('mrp_action', '=', 'none')],
['mrp_date', 'mrp_qty'], ['mrp_date:day'],
)
supply_qty_by_date = {}
for group in supply_groups:
# Reformat date back to default server format.
group_date = datetime.strptime(
group['mrp_date:day'], ODOO_READ_GROUP_DAY_FORMAT).strftime(
DEFAULT_SERVER_DATE_FORMAT)
supply_qty_by_date[group_date] = group['mrp_qty']
# Read supply actions
# TODO: if we remove cancel take it into account here,
# TODO: as well as mrp_type ('r').
exclude_mrp_actions = ['none', 'cancel']
action_groups = mrp_move_obj.read_group(
[('mrp_product_id', '=', mrp_product.id),
('mrp_qty', '!=', 0.0),
('mrp_type', '=', 's'),
('mrp_action', 'not in', exclude_mrp_actions)],
['mrp_date', 'mrp_qty'], ['mrp_date:day'],
)
supply_actions_qty_by_date = {}
for group in action_groups:
# Reformat date back to default server format.
group_date = datetime.strptime(
group['mrp_date:day'], ODOO_READ_GROUP_DAY_FORMAT).strftime(
DEFAULT_SERVER_DATE_FORMAT)
supply_actions_qty_by_date[group_date] = group['mrp_qty']
# Dates
mrp_dates = set(mrp_move_obj.search([
('mrp_product_id', '=', mrp_product.id)],
order='mrp_date').mapped('mrp_date'))
on_hand_qty = mrp_product.current_qty_available # TODO: unreserved?
for mdt in sorted(mrp_dates):
mrp_inventory_data = {
'mrp_product_id': mrp_product.id,
'date': mdt,
}
demand_qty = 0.0
supply_qty = 0.0
if mdt in demand_qty_by_date.keys():
demand_qty = demand_qty_by_date[mdt]
mrp_inventory_data['demand_qty'] = abs(demand_qty)
if mdt in supply_qty_by_date.keys():
supply_qty = supply_qty_by_date[mdt]
mrp_inventory_data['supply_qty'] = abs(supply_qty)
if mdt in supply_actions_qty_by_date.keys():
mrp_inventory_data['to_procure'] = \
supply_actions_qty_by_date[mdt]
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
self.env['mrp.inventory'].create(mrp_inventory_data)
@api.model
def _mrp_final_process(self):
logger.info('START MRP FINAL PROCESS')
mrp_product_ids = self.env['mrp.product'].search([
('mrp_llc', '<', 9999),
('mrp_area_id', '!=', False)])
for mrp_product in mrp_product_ids:
# Build the time-phased inventory
self._init_mrp_inventory(mrp_product)
# Complete info on mrp_move (running availability and nbr actions)
qoh = mrp_product.mrp_qty_available
moves = self.env['mrp.move'].search([
('mrp_product_id', '=', mrp_product.id)],
order='mrp_date, mrp_type desc, id')
for move in moves:
qoh = qoh + move.mrp_qty
move.running_availability = qoh
nbr_actions = mrp_product.mrp_move_ids.filtered(
lambda m: m.mrp_action != 'none')
horizon_4w = fields.Date.to_string(
date.today() + timedelta(weeks=4))
nbr_actions_4w = nbr_actions.filtered(
lambda m: m.mrp_action_date < horizon_4w)
if nbr_actions:
mrp_product.write({
'nbr_mrp_actions': len(nbr_actions),
'nbr_mrp_actions_4w': len(nbr_actions_4w),
})
logger.info('END MRP FINAL PROCESS')
@api.multi
def run_mrp_multi_level(self):
self._mrp_cleanup()
mrp_lowest_llc = self._low_level_code_calculation()
self._calculate_mrp_applicable()
self._mrp_initialisation()
self._mrp_calculation(mrp_lowest_llc)
self._mrp_final_process()

View File

@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<odoo>
<record id="view_run_mrp_multi_level_wizard" model="ir.ui.view">
<field name="name">Run MRP</field>
<field name="model">mrp.multi.level</field>
<field name="arch" type="xml">
<form string="Run Multi Level MRP">
<footer>
<button name="run_mrp_multi_level" string="Run MRP" type="object" class="oe_highlight" />
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<act_window name="Run MRP"
res_model="mrp.multi.level"
src_model="mrp.multi.level"
view_mode="form"
target="new"
key2="client_action_multi"
id="action_mrp_multi_level"/>
</odoo>