Merge PR #1411 into 18.0

Signed-off-by LoisRForgeFlow
This commit is contained in:
OCA-git-bot
2025-01-10 12:32:21 +00:00
59 changed files with 11548 additions and 0 deletions

271
mrp_multi_level/README.rst Normal file
View File

@@ -0,0 +1,271 @@
===============
MRP Multi Level
===============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:afddaf576893642eae579ee762efefb3c3ecc9f84445b23b43ab6fc448975fce
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
:target: https://odoo-community.org/page/development-status
:alt: Production/Stable
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github
:target: https://github.com/OCA/manufacture/tree/18.0/mrp_multi_level
:alt: OCA/manufacture
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/manufacture-18-0/manufacture-18-0-mrp_multi_level
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/manufacture&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
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 set by product variant MRP area pairs.
- Cron job to calculate the MRP demand.
- Manually calculate the MRP demand.
- Confirm the calculated MRP demand and create PO's, or MO's.
- Able to see the products for which action is needed throught Planned
Orders.
- Integration with `Stock Demand
Estimates <https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_demand_estimate>`__
system. Note: You need to install `mrp_multi_level_estimate
module <https://github.com/OCA/manufacture/tree/12.0/mrp_multi_level_estimate>`__.
**Table of contents**
.. contents::
:local:
Configuration
=============
MRP Areas
~~~~~~~~~
- Go to *Manufacturing > Configuration > MRP Areas* and define or edit
any existing area. You can specify the working hours for every area.
Product MRP Area Parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Go to *Manufacturing > Products > Product MRP Area Parameters* and
set the MRP parameters for a given product and area.
Usage
=====
To manually run the MRP scheduler:
1. Go to *Manufacturing > Planning > Run MRP Multi Level*.
2. On the wizard click *Run MRP*.
To launch replenishment orders (moves, purchases, production orders...):
1. Go to *Manufacturing > Planning > MRP Inventory*.
2. Filter with *To procure*.
3. Select multiple records and click on *Action > Procure* or click the
right hand side gears in any record.
4. On the wizard, check everything is ok and click *Execute*.
Changelog
=========
13.0.1.5.0 (2020-04-09)
-----------------------
**Features**
- Show *Run MRP Multi Level* menu only to a specific new security group
*Run MRP Manually*.
(`#492 <https://github.com/OCA/manufacture/issues/492>`__)
13.0.1.4.0 (2020-03-26)
-----------------------
- Add menu entry for planned orders
- Add button to navigate from planned orders to linked manufacturing
orders
- Add action to convert planned orders to fixed
- When changing the due date in a planned order the release date is
recomputed
13.0.1.3.0 (2020-03-02)
-----------------------
- [IMP] Minor changes"
(`#470 <https://github.com/OCA/manufacture/pull/470>`__).
- Planned Order release and due date become required.
- Add button to Product MRP Area to update MOQ from Supplier Info.
- Link Manufacturing Orders with Planned Orders.
- Allow Mrp Inventory Procure Wizard to be used from other models.
- Make MRP Inventory creation more extensible.
- Main Supplier computation (v13 requires explicit False
definitions)
13.0.1.2.0 (2020-02-20)
-----------------------
- [IMP] Minor changes
(`#468 <https://github.com/OCA/manufacture/pull/468>`__).
- Planned Orders become fixed on manual creation by default
- Released Quantity becomes readonly
- Add product reference if Planned Order name is not defined on bom
explosion
13.0.1.1.0 (2020-02-21)
-----------------------
- [FIX] Minor changes
(`#469 <https://github.com/OCA/manufacture/pull/469>`__).
- Fix Main supplier computation in multi company
- Drop Triplicated field in search view
- [IMP] Minor changes
(`#463 <https://github.com/OCA/manufacture/pull/463>`__).
- Show supply method on MRP Inventory
- Allow no-MRP users to look into Products
13.0.1.0.0 (2019-12-18)
-----------------------
- [MIG] Migration to v13.
12.0.1.0.0 (2019-08-05)
-----------------------
- [MIG] Migration to v12:
- Estimates as a forecasting mechanism is moved to a new module
(mrp_multi_level_estimate).
11.0.3.0.0 (2019-05-22)
-----------------------
- [REW/IMP] Rework to include Planned Orders.
(`#365 <https://github.com/OCA/manufacture/pull/365>`__).
- [IMP] Able to procure from a different location than the area's
location.
11.0.2.2.0 (2019-05-02)
-----------------------
- [IMP] Able to run MRP only for selected areas.
(`#360 <https://github.com/OCA/manufacture/pull/360>`__).
11.0.2.1.0 (2019-04-02)
-----------------------
- [IMP] Implement *Nbr. Days* functionality to be able to group demand
when generating supply proposals.
(`#345 <https://github.com/OCA/manufacture/pull/345>`__).
11.0.2.0.0 (2018-11-20)
-----------------------
- [REW] Refactor MRP Area.
(`#322 <https://github.com/OCA/manufacture/pull/322>`__):
- MRP product concept dropped in favor of *Product MRP Area
Parameters*. This allow to set different MRP parameters for the
same product in different areas.
- Menu items reordering.
11.0.1.1.0 (2018-08-30)
-----------------------
- [FIX] Consider *Qty Multiple* on product to propose the quantity to
procure. (`#297 <https://github.com/OCA/manufacture/pull/297>`__)
11.0.1.0.1 (2018-08-03)
-----------------------
- [FIX] User and system locales doesn't break MRP calculation.
(`#290 <https://github.com/OCA/manufacture/pull/290>`__)
- [FIX] Working Hours are now defined only at Warehouse level and
displayed as a related on MRP Areas.
(`#290 <https://github.com/OCA/manufacture/pull/290>`__)
11.0.1.0.0 (2018-07-09)
-----------------------
- Start of the history.
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 to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_multi_level%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Ucamco
* ForgeFlow
Contributors
------------
- Wim Audenaert <wim.audenaert@ucamco.com>
- Jordi Ballester <jordi.ballester@forgeflow.com>
- Lois Rilo <lois.rilo@forgeflow.com>
- Héctor Villarreal <hector.villarreal@forgeflow.com>
- Christopher Ormaza <chris.ormaza@forgeflow.com>
- Joan Sisquella <joan.sisquella@forgeflow.com>
- Alexandre Fayolle <alexandre.fayolle@camptocamp.com>
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
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.
.. |maintainer-JordiBForgeFlow| image:: https://github.com/JordiBForgeFlow.png?size=40px
:target: https://github.com/JordiBForgeFlow
:alt: JordiBForgeFlow
.. |maintainer-LoisRForgeFlow| image:: https://github.com/LoisRForgeFlow.png?size=40px
:target: https://github.com/LoisRForgeFlow
:alt: LoisRForgeFlow
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-JordiBForgeFlow| |maintainer-LoisRForgeFlow|
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/18.0/mrp_multi_level>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -0,0 +1,45 @@
# Copyright 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-21 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
{
"name": "MRP Multi Level",
"version": "18.0.1.0.0",
"development_status": "Production/Stable",
"license": "LGPL-3",
"author": "Ucamco, ForgeFlow, Odoo Community Association (OCA)",
"maintainers": ["JordiBForgeFlow", "LoisRForgeFlow"],
"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",
"data/system_parameter.xml",
"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",
"views/mrp_planned_order_views.xml",
"wizards/mrp_multi_level_views.xml",
"views/mrp_move_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",
],
"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,12 @@
<?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="state">code</field>
<field name="code">model.run_mrp_multi_level()</field>
</record>
</odoo>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="llc_calculation_recursion_limit" model="ir.config_parameter">
<field name="key">mrp_multi_level.llc_calculation_recursion_limit</field>
<field name="value">1000</field>
</record>
</odoo>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="stock_inventory_1" model="stock.quant">
<field name="product_id" ref="product_product_pp_1" />
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="inventory_quantity">10</field>
<field
name="location_id"
model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"
/>
</record>
<record id="stock_inventory_2" model="stock.quant">
<field name="product_id" ref="product_product_pp_2" />
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="inventory_quantity">20</field>
<field
name="location_id"
model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"
/>
</record>
<record id="stock_inventory_3" model="stock.quant">
<field name="product_id" ref="product_product_sf_2" />
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="inventory_quantity">15</field>
<field
name="location_id"
model="stock.location"
eval="obj().env.ref('stock.warehouse0').lot_stock_id.id"
/>
</record>
<function model="stock.quant" name="action_apply_inventory">
<function
eval="[[('id', 'in', (ref('stock_inventory_1'),
ref('stock_inventory_2'),
ref('stock_inventory_3'),
))]]"
model="stock.quant"
name="search"
/>
</function>
</odoo>

View File

@@ -0,0 +1,178 @@
<?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="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="produce_delay">2</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="uom.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="uom.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="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="produce_delay">1</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="uom.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="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_2" />
</record>
<!-- FP-3 -->
<record id="mrp_bom_fp_3" model="mrp.bom">
<field name="product_tmpl_id" ref="product_product_fp_3_product_template" />
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="produce_delay">3</field>
</record>
<record id="mrp_bom_fp_3_line_sf_3" model="mrp.bom.line">
<field name="product_id" ref="product_product_sf_3" />
<field name="product_qty">2</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_3" />
</record>
<record id="mrp_bom_fp_3_line_pp_3" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_3" />
<field name="product_qty">2</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_fp_3" />
</record>
<!-- Customizable Desk -->
<record id="mrp_bom_product_4" model="mrp.bom">
<field
name="product_tmpl_id"
ref="product.product_product_4_product_template"
/>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">5</field>
</record>
<record id="mrp_bom_product_4_line_av_11" model="mrp.bom.line">
<field name="product_id" ref="product_product_av_11" />
<field name="product_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">1</field>
<field name="bom_id" ref="mrp_bom_product_4" />
<field
name="bom_product_template_attribute_value_ids"
eval="[(6, 0, [ref('product.product_4_attribute_1_value_1')])]"
/>
</record>
<record id="mrp_bom_product_4_line_av_12" model="mrp.bom.line">
<field name="product_id" ref="product_product_av_12" />
<field name="product_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">2</field>
<field name="bom_id" ref="mrp_bom_product_4" />
<field
name="bom_product_template_attribute_value_ids"
eval="[(6, 0, [ref('product.product_4_attribute_1_value_2')])]"
/>
</record>
<record id="mrp_bom_product_4_line_av_21" model="mrp.bom.line">
<field name="product_id" ref="product_product_av_21" />
<field name="product_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">3</field>
<field name="bom_id" ref="mrp_bom_product_4" />
<field
name="bom_product_template_attribute_value_ids"
eval="[(6, 0, [ref('product.product_4_attribute_2_value_1')])]"
/>
</record>
<record id="mrp_bom_product_4_line_av_22" model="mrp.bom.line">
<field name="product_id" ref="product_product_av_22" />
<field name="product_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">4</field>
<field name="bom_id" ref="mrp_bom_product_4" />
<field
name="bom_product_template_attribute_value_ids"
eval="[(6, 0, [ref('product.product_4_attribute_2_value_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="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="produce_delay">1</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="uom.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="uom.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="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="produce_delay">3</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="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_sf_2" />
</record>
<!-- SF-3 -->
<record id="mrp_bom_sf_3" model="mrp.bom">
<field name="product_tmpl_id" ref="product_product_sf_3_product_template" />
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="type">phantom</field>
<field name="sequence">5</field>
<field name="produce_delay">3</field>
</record>
<record id="mrp_bom_sf_3_line_pp_3" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_3" />
<field name="product_qty">1</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_sf_3" />
</record>
<record id="mrp_bom_sf_3_line_pp_4" model="mrp.bom.line">
<field name="product_id" ref="product_product_pp_4" />
<field name="product_qty">3</field>
<field name="product_uom_id" ref="uom.product_uom_unit" />
<field name="sequence">5</field>
<field name="bom_id" ref="mrp_bom_sf_3" />
</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,71 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="product_mrp_area_fp_1" model="product.mrp.area">
<field name="product_id" ref="product_product_fp_1" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_fp_2" model="product.mrp.area">
<field name="product_id" ref="product_product_fp_2" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_fp_3" model="product.mrp.area">
<field name="product_id" ref="product_product_fp_3" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_product_4" model="product.mrp.area">
<field name="product_id" ref="product.product_product_4" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_product_4b" model="product.mrp.area">
<field name="product_id" ref="product.product_product_4b" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_product_4c" model="product.mrp.area">
<field name="product_id" ref="product.product_product_4c" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_sf_1" model="product.mrp.area">
<field name="product_id" ref="product_product_sf_1" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_sf_2" model="product.mrp.area">
<field name="product_id" ref="product_product_sf_2" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_sf_3" model="product.mrp.area">
<field name="product_id" ref="product_product_sf_3" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_pp_1" model="product.mrp.area">
<field name="product_id" ref="product_product_pp_1" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_pp_2" model="product.mrp.area">
<field name="product_id" ref="product_product_pp_2" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_pp_3" model="product.mrp.area">
<field name="product_id" ref="product_product_pp_3" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_pp_4" model="product.mrp.area">
<field name="product_id" ref="product_product_pp_4" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_av_11" model="product.mrp.area">
<field name="product_id" ref="product_product_av_11" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_av_12" model="product.mrp.area">
<field name="product_id" ref="product_product_av_12" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_av_21" model="product.mrp.area">
<field name="product_id" ref="product_product_av_21" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
<record id="product_mrp_area_av_22" model="product.mrp.area">
<field name="product_id" ref="product_product_av_22" />
<field name="mrp_area_id" ref="mrp_area_stock_wh0" />
</record>
</odoo>

View File

@@ -0,0 +1,176 @@
<?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="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<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="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
/>
</record>
<record id="product_product_fp_3" model="product.product">
<field name="name">FP-3</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
/>
</record>
<record id="product_product_fp_4" model="product.product">
<field name="name">FP-4</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
</record>
<!-- Customizable Desk -->
<record id="product.product_product_4_product_template" model="product.template">
<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="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<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="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
/>
</record>
<record id="product_product_sf_3" model="product.product">
<field name="name">SF-3</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<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="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.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="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
<record id="product_product_pp_3" model="product.product">
<field name="name">PP-3</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
<record id="product_product_pp_4" model="product.product">
<field name="name">PP-4</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
<record id="product_product_av_11" model="product.product">
<field name="name">AV-11 steel</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
<record id="product_product_av_12" model="product.product">
<field name="name">AV-12 aluminium</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
<record id="product_product_av_21" model="product.product">
<field name="name">AV-21 white</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
<record id="product_product_av_22" model="product.product">
<field name="name">AV-22 black</field>
<field name="categ_id" ref="product_category_mrp" />
<field name="is_storable" eval="True" />
<field name="uom_id" ref="uom.product_uom_unit" />
<field name="uom_po_id" ref="uom.product_uom_unit" />
<field
name="route_ids"
eval="[(6, 0, [ref('purchase_stock.route_warehouse0_buy')])]"
/>
</record>
</odoo>

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="product_supplierinfo_av_11" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_av_11_product_template" />
<field name="partner_id" 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_av_12" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_av_12_product_template" />
<field name="partner_id" 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_av_21" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_av_21_product_template" />
<field name="partner_id" 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_av_22" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_av_22_product_template" />
<field name="partner_id" 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_1" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_pp_1_product_template" />
<field name="partner_id" 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="partner_id" ref="res_partner_lazer_tech" />
<field name="delay">2</field>
<field name="min_qty">0</field>
<field name="price">100</field>
</record>
<record id="product_supplierinfo_pp_3" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_pp_3_product_template" />
<field name="partner_id" ref="res_partner_lazer_tech" />
<field name="delay">2</field>
<field name="min_qty">0</field>
<field name="price">10</field>
</record>
<record id="product_supplierinfo_pp_4" model="product.supplierinfo">
<field name="product_tmpl_id" ref="product_product_pp_4_product_template" />
<field name="partner_id" ref="res_partner_lazer_tech" />
<field name="delay">3</field>
<field name="min_qty">0</field>
<field name="price">80</field>
</record>
</odoo>

View File

@@ -0,0 +1,8 @@
<?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" />
</record>
</odoo>

Binary file not shown.

1164
mrp_multi_level/i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

1108
mrp_multi_level/i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

1024
mrp_multi_level/i18n/hr.po Normal file

File diff suppressed because it is too large Load Diff

1045
mrp_multi_level/i18n/it.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
from . import mrp_area
from . import stock_location
from . import product_product
from . import product_template
from . import mrp_move
from . import mrp_planned_order
from . import mrp_inventory
from . import product_mrp_area
from . import stock_rule
from . import mrp_production
from . import stock_quant

View File

@@ -0,0 +1,42 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-21 ForgeFlow S.L. (https://www.forgeflow.com)
# - Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
# - Lois Rilo Antelo <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import api, fields, models
class MrpArea(models.Model):
_name = "mrp.area"
_description = "MRP Area"
name = fields.Char(required=True)
warehouse_id = fields.Many2one(
comodel_name="stock.warehouse", string="Warehouse", required=True
)
company_id = fields.Many2one(
comodel_name="res.company", related="warehouse_id.company_id", store=True
)
location_id = fields.Many2one(
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",
)
@api.model
def _datetime_to_date_tz(self, dt_to_convert=None):
"""Coverts a datetime to date considering the timezone of MRP Area.
If no datetime is provided, it returns today's date in the timezone."""
return fields.Date.context_today(
self.with_context(tz=self.calendar_id.tz),
timestamp=dt_to_convert,
)
def _get_locations(self):
self.ensure_one()
return self.location_id

View File

@@ -0,0 +1,135 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-21 ForgeFlow S.L. (https://www.forgeflow.com)
# - Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
# - Lois Rilo Antelo <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from datetime import date, timedelta
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"
# 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,
)
product_mrp_area_id = fields.Many2one(
comodel_name="product.mrp.area",
string="Product Parameters",
index=True,
required=True,
ondelete="cascade",
)
company_id = fields.Many2one(
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",
store=True,
)
uom_id = fields.Many2one(
comodel_name="uom.uom", string="Product UoM", compute="_compute_uom_id"
)
date = fields.Date()
demand_qty = fields.Float(string="Demand")
supply_qty = fields.Float(string="Supply")
initial_on_hand_qty = fields.Float(string="Starting Inventory", aggregator="avg")
final_on_hand_qty = fields.Float(string="Forecasted Inventory", aggregator="avg")
to_procure = fields.Float(compute="_compute_to_procure", store=True)
running_availability = fields.Float(
string="Planned Availability",
aggregator="avg",
help="Theoretical inventory level if all planned orders were released.",
)
order_release_date = fields.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
)
supply_method = fields.Selection(
string="Supply Method",
related="product_mrp_area_id.supply_method",
readonly=True,
store=True,
)
main_supplier_id = fields.Many2one(
string="Main Supplier",
related="product_mrp_area_id.main_supplier_id",
readonly=True,
store=True,
)
mrp_planner_id = fields.Many2one(
related="product_mrp_area_id.mrp_planner_id",
readonly=True,
store=True,
)
def _compute_uom_id(self):
for rec in self:
rec.uom_id = rec.product_mrp_area_id.product_id.uom_id
@api.depends("planned_order_ids", "planned_order_ids.qty_released")
def _compute_to_procure(self):
for rec in self:
rec.to_procure = (
0.0
if rec.supply_method == "phantom"
else sum(rec.planned_order_ids.mapped("mrp_qty"))
- sum(rec.planned_order_ids.mapped("qty_released"))
)
@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):
delay = rec.product_mrp_area_id.mrp_lead_time
if delay and rec.mrp_area_id.calendar_id:
dt_date = fields.Datetime.to_datetime(rec.date)
# 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()
elif delay:
order_release_date = fields.Date.from_string(rec.date) - timedelta(
days=delay
)
else:
order_release_date = rec.date
if order_release_date < today:
order_release_date = today
rec.order_release_date = order_release_date
def action_open_planned_orders(self):
planned_order_ids = []
for rec in self:
planned_order_ids += rec.planned_order_ids.ids
domain = [("id", "in", planned_order_ids)]
return {
"name": _("Planned Orders"),
"type": "ir.actions.act_window",
"res_model": "mrp.planned.order",
"view_mode": "list,form",
"domain": domain,
}

View File

@@ -0,0 +1,95 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-19 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class MrpMove(models.Model):
_name = "mrp.move"
_description = "MRP Move"
_order = "product_mrp_area_id, mrp_date, mrp_type desc, id"
# TODO: too many indexes...
product_mrp_area_id = fields.Many2one(
comodel_name="product.mrp.area",
string="Product MRP Area",
index=True,
required=True,
ondelete="cascade",
)
mrp_area_id = fields.Many2one(
comodel_name="mrp.area",
related="product_mrp_area_id.mrp_area_id",
string="MRP Area",
store=True,
index=True,
)
company_id = fields.Many2one(
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",
store=True,
)
current_date = fields.Date()
current_qty = fields.Float()
mrp_date = fields.Date(string="MRP Date")
planned_order_up_ids = fields.Many2many(
comodel_name="mrp.planned.order",
relation="mrp_move_planned_order_rel",
column1="move_down_id",
column2="order_id",
string="Planned Orders UP",
)
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"
)
name = fields.Char(string="Description")
origin = fields.Char(string="Source Document")
parent_product_id = fields.Many2one(
comodel_name="product.product", string="Parent Product", index=True
)
production_id = fields.Many2one(
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
)
purchase_order_id = fields.Many2one(
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"),
],
)
stock_move_id = fields.Many2one(
comodel_name="stock.move", string="Stock Move", index=True
)
bom_id = fields.Many2one("mrp.bom")

View File

@@ -0,0 +1,115 @@
# Copyright 2019 ForgeFlow S.L. (https://www.forgeflow.com)
# - Lois Rilo Antelo <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from datetime import timedelta
from odoo import api, fields, models
class MrpPlannedOrder(models.Model):
_name = "mrp.planned.order"
_description = "Planned Order"
_order = "due_date, id"
name = fields.Char(string="Description")
origin = fields.Char(string="Source Document")
product_mrp_area_id = fields.Many2one(
comodel_name="product.mrp.area",
string="Product MRP Area",
index=True,
required=True,
ondelete="cascade",
)
mrp_area_id = fields.Many2one(
comodel_name="mrp.area",
related="product_mrp_area_id.mrp_area_id",
string="MRP Area",
store=True,
index=True,
readonly=True,
)
company_id = fields.Many2one(
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",
store=True,
readonly=True,
)
order_release_date = fields.Date(
string="Release Date", help="Order release date planned by MRP.", required=True
)
due_date = fields.Date(
help="Date in which the supply must have been completed.",
required=True,
)
qty_released = fields.Float(readonly=True)
fixed = fields.Boolean(default=True)
mrp_qty = fields.Float(string="Quantity")
mrp_move_down_ids = fields.Many2many(
comodel_name="mrp.move",
relation="mrp_move_planned_order_rel",
column1="order_id",
column2="move_down_id",
string="MRP Move DOWN",
)
mrp_action = fields.Selection(
selection=[
("manufacture", "Manufacturing Order"),
("phantom", "Kit"),
("buy", "Purchase Order"),
("pull", "Pull From"),
("push", "Push To"),
("pull_push", "Pull & Push"),
("none", "None"),
],
string="Action",
)
mrp_inventory_id = fields.Many2one(
string="Associated MRP Inventory",
comodel_name="mrp.inventory",
ondelete="set null",
)
mrp_production_ids = fields.One2many(
"mrp.production", "planned_order_id", string="Manufacturing Orders"
)
mo_count = fields.Integer(compute="_compute_mrp_production_count")
mrp_planner_id = fields.Many2one(
related="product_mrp_area_id.mrp_planner_id",
readonly=True,
store=True,
)
def _compute_mrp_production_count(self):
for rec in self:
rec.mo_count = len(rec.mrp_production_ids)
@api.onchange("due_date")
def _onchange_due_date(self):
if self.due_date:
if self.product_mrp_area_id.mrp_lead_time:
calendar = self.mrp_area_id.calendar_id
if calendar:
dt = fields.Datetime.from_string(self.due_date)
res = calendar.plan_days(
-1 * (self.product_mrp_area_id.mrp_lead_time + 1), dt
)
self.order_release_date = res.date()
else:
self.order_release_date = fields.Date.from_string(
self.due_date
) - timedelta(days=self.product_mrp_area_id.mrp_lead_time)
def action_toggle_fixed(self):
for rec in self:
rec.fixed = not rec.fixed
def action_open_linked_mrp_production(self):
action = self.env.ref("mrp.mrp_production_action")
result = action.read()[0]
result["context"] = {}
result["domain"] = f"[('id','in',{self.mrp_production_ids.ids})]"
return result

View File

@@ -0,0 +1,15 @@
# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com)
# - Héctor Villarreal <hector.villarreal@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class MrpProduction(models.Model):
"""Manufacturing Orders"""
_inherit = "mrp.production"
planned_order_id = fields.Many2one(
comodel_name="mrp.planned.order",
index=True,
)

View File

@@ -0,0 +1,312 @@
# Copyright 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-19 ForgeFlow S.L. (https://www.forgeflow.com)
# - Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
# - Lois Rilo Antelo <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from math import ceil
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ProductMRPArea(models.Model):
_name = "product.mrp.area"
_description = "Product MRP Area"
active = fields.Boolean(default=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",
store=True,
)
product_id = fields.Many2one(
comodel_name="product.product",
required=True,
string="Product",
ondelete="cascade",
)
product_tmpl_id = fields.Many2one(
comodel_name="product.template",
readonly=True,
related="product_id.product_tmpl_id",
store=True,
)
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.",
)
# 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_minimum_stock = fields.Float(string="Safety 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_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",
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")
distribution_lead_time = fields.Float()
main_supplier_id = fields.Many2one(
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,
)
supply_method = fields.Selection(
selection=[
("buy", "Buy"),
("none", "Undefined"),
("manufacture", "Produce"),
("phantom", "Kit"),
("pull", "Pull From"),
("push", "Push To"),
("pull_push", "Pull & Push"),
],
compute="_compute_supply_method",
)
supply_bom_id = fields.Many2one(
comodel_name="mrp.bom",
string="Supply BoM",
compute="_compute_supply_method",
)
qty_available = fields.Float(
string="Quantity Available", compute="_compute_qty_available"
)
mrp_move_ids = fields.One2many(
comodel_name="mrp.move", inverse_name="product_mrp_area_id", readonly=True
)
planned_order_ids = fields.One2many(
comodel_name="mrp.planned.order",
inverse_name="product_mrp_area_id",
readonly=True,
)
mrp_planner_id = fields.Many2one("res.users")
_sql_constraints = [
(
"product_mrp_area_uniq",
"unique(product_id, mrp_area_id)",
"The product/MRP Area parameters combination must be unique.",
)
]
@api.constrains(
"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",
]
)
for rec in values:
if any(v < 0 for v in rec.values()):
raise ValidationError(_("You cannot use a negative number."))
def _compute_display_name(self):
for area in self:
area.display_name = (
f"[{area.mrp_area_id.name}] {area.product_id.display_name}"
)
@api.model
def _search_display_name(self, operator, value):
domain = super()._search_display_name(operator, value)
if operator in ("ilike", "like", "=", "=like", "=ilike"):
domain = [
"|",
"|",
("product_id.name", operator, value),
("product_id.default_code", operator, value),
("mrp_area_id.name", operator, value),
]
return domain
return domain
def _compute_mrp_lead_time(self):
produced = self.filtered(lambda r: r.supply_method == "manufacture")
purchased = self.filtered(lambda r: r.supply_method == "buy")
distributed = self.filtered(
lambda r: r.supply_method in ("pull", "push", "pull_push")
)
for rec in produced:
rec.mrp_lead_time = sum(rec.product_id.mapped("bom_ids.produce_delay"))
for rec in purchased:
rec.mrp_lead_time = rec.main_supplierinfo_id.delay
for rec in distributed:
rec.mrp_lead_time = rec.distribution_lead_time
for rec in self - produced - purchased - distributed:
rec.mrp_lead_time = 0
def _compute_qty_available(self):
for rec in self:
rec.qty_available = rec.product_id.with_context(
location=rec._get_locations().ids
).qty_available
def _get_rule(self):
self.ensure_one()
group_obj = self.env["procurement.group"]
proc_loc = self.location_proc_id or self.location_id
values = {
"warehouse_id": self.mrp_area_id.warehouse_id,
"company_id": self.company_id,
}
rule = group_obj._get_rule(self.product_id, proc_loc, values)
if not rule:
return False
# Keep getting the rule for the product and the source location until the
# action is "buy" or "manufacture". Or until the action is "Pull From" or
# "Pull & Push" and the supply method is "Take from Stock".
while rule.action not in ("buy", "manufacture") and rule.procure_method in (
"make_to_order",
"mts_else_mto",
):
new_rule = group_obj._get_rule(
self.product_id, rule.location_src_id, values
)
if not new_rule:
break
rule = new_rule
return rule
def _compute_supply_method(self):
boms_by_product = self.env["mrp.bom"]._bom_find(self.mapped("product_id"))
for rec in self:
rule = rec._get_rule()
bom = boms_by_product.get(rec.product_id, self.env["mrp.bom"])
if bom.type == "phantom":
rec.supply_method = "phantom"
rec.supply_bom_id = bom
elif not rule:
rec.supply_method = "none"
rec.supply_bom_id = False
elif rule.action == "manufacture":
rec.supply_method = rule.action
rec.supply_bom_id = bom
else:
rec.supply_method = rule.action
rec.supply_bom_id = False
@api.depends(
"mrp_area_id", "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"):
suppliers = rec.product_id.seller_ids.filtered(
lambda r, rec=rec: (not r.product_id or r.product_id == rec.product_id)
and (not r.company_id or r.company_id == rec.company_id)
).sorted(lambda s: (s.sequence, -s.min_qty, s.price, s.id))
if not suppliers:
rec.main_supplierinfo_id = False
rec.main_supplier_id = False
continue
rec.main_supplierinfo_id = suppliers[0]
rec.main_supplier_id = suppliers[0].partner_id
for rec in self.filtered(lambda r: r.supply_method != "buy"):
rec.main_supplierinfo_id = False
rec.main_supplier_id = False
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
):
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:
return self.mrp_maximum_order_qty
return qty_to_order
def update_min_qty_from_main_supplier(self):
for rec in self.filtered(
lambda r: r.main_supplierinfo_id and r.supply_method == "buy"
):
rec.mrp_minimum_order_qty = rec.main_supplierinfo_id.min_qty
def _in_stock_moves_domain(self):
self.ensure_one()
locations = self._get_locations()
return [
("product_id", "=", self.product_id.id),
("state", "not in", ["done", "cancel"]),
("product_qty", ">", 0.00),
"!",
("location_id", "child_of", locations.ids),
("location_dest_id", "child_of", locations.ids),
]
def _out_stock_moves_domain(self):
self.ensure_one()
locations = self._get_locations()
return [
("product_id", "=", self.product_id.id),
("state", "not in", ["done", "cancel"]),
("product_qty", ">", 0.00),
("location_id", "child_of", locations.ids),
"!",
("location_dest_id", "child_of", locations.ids),
]
def action_view_stock_moves(self, domain):
self.ensure_one()
action = self.env.ref("stock.stock_move_action").read()[0]
action["domain"] = domain
action["context"] = {}
return action
def action_view_incoming_stock_moves(self):
return self.action_view_stock_moves(self._in_stock_moves_domain())
def action_view_outgoing_stock_moves(self):
return self.action_view_stock_moves(self._out_stock_moves_domain())
def _to_be_exploded(self):
self.ensure_one()
return self.supply_method in ["manufacture", "phantom"]
def _get_locations(self):
self.ensure_one()
return self.mrp_area_id._get_locations()
def _should_create_planned_order(self):
self.ensure_one()
return True

View File

@@ -0,0 +1,71 @@
# Copyright 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-19 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
import ast
from odoo import fields, models
class Product(models.Model):
_inherit = "product.product"
llc = fields.Integer(string="Low Level Code", default=0, index=True)
manufacturing_order_ids = fields.One2many(
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",
)
mrp_area_ids = fields.One2many(
comodel_name="product.mrp.area",
inverse_name="product_id",
string="MRP Area parameters",
)
mrp_area_count = fields.Integer(
string="MRP Area Parameter Count",
readonly=True,
compute="_compute_mrp_area_count",
)
def _compute_mrp_area_count(self):
for rec in self:
rec.mrp_area_count = len(rec.mrp_area_ids)
def write(self, values):
res = super().write(values)
if values.get("active") is False:
parameters = (
self.env["product.mrp.area"]
.sudo()
.search([("product_id", "in", self.ids)])
)
parameters.write({"active": False})
return res
def action_view_mrp_area_parameters(self):
self.ensure_one()
result = self.env["ir.actions.actions"]._for_xml_id(
"mrp_multi_level.product_mrp_area_action"
)
ctx = ast.literal_eval(result.get("context"))
if not ctx:
ctx = {}
mrp_areas = self.env["mrp.area"].search([])
if len(mrp_areas) == 1:
ctx.update({"default_mrp_area_id": mrp_areas[0].id})
area_ids = self.mrp_area_ids.ids
ctx.update({"default_product_id": self.id})
if self.mrp_area_count != 1:
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
return result

View File

@@ -0,0 +1,49 @@
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
import ast
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
mrp_area_ids = fields.One2many(
comodel_name="product.mrp.area",
inverse_name="product_tmpl_id",
string="MRP Area parameters",
)
mrp_area_count = fields.Integer(
string="MRP Area Parameter Count",
readonly=True,
compute="_compute_mrp_area_count",
)
def _compute_mrp_area_count(self):
for rec in self:
rec.mrp_area_count = len(rec.mrp_area_ids)
def action_view_mrp_area_parameters(self):
self.ensure_one()
result = self.env["ir.actions.actions"]._for_xml_id(
"mrp_multi_level.product_mrp_area_action"
)
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
if len(self.product_variant_ids) == 1:
variant = self.product_variant_ids[0]
ctx.update({"default_product_id": variant.id})
if len(mrp_area_ids) != 1:
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
return result

View File

@@ -0,0 +1,41 @@
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-19 ForgeFlow S.L. (https://www.forgeflow.com)
# - Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
import ast
from odoo import fields, models
class StockLocation(models.Model):
_inherit = "stock.location"
mrp_area_count = fields.Integer(
string="MRP Area Parameter Count",
readonly=True,
compute="_compute_mrp_area_count",
)
def _compute_mrp_area_count(self):
for rec in self:
areas = self.env["mrp.area"].search([("location_id", "=", rec.id)])
rec.mrp_area_count = len(areas)
def action_view_mrp_area_location(self):
self.ensure_one()
result = self.env["ir.actions.actions"]._for_xml_id(
"mrp_multi_level.mrp_area_action"
)
ctx = ast.literal_eval(result.get("context"))
if not ctx:
ctx = {}
mrp_areas = self.env["mrp.area"].search([("location_id", "=", self.id)])
if self.mrp_area_count != 1:
result["domain"] = [("id", "in", mrp_areas.ids)]
else:
ctx.update({"default_mrp_area_id": mrp_areas[0].id})
res = self.env.ref("mrp_multi_level.mrp_area_form", False)
result["views"] = [(res and res.id or False, "form")]
result["res_id"] = mrp_areas[0].id
result["context"] = ctx
return result

View File

@@ -0,0 +1,16 @@
# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com)
# - Joan Sisquella Andrés <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import api, models
class StockQuant(models.Model):
_inherit = "stock.quant"
@api.model
def _get_inventory_fields_write(self):
"""
Adding field product_uom_id, inventory_quantity
"""
fields = super()._get_inventory_fields_write()
return fields + ["product_uom_id", "inventory_quantity"]

View File

@@ -0,0 +1,35 @@
# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com)
# - Héctor Villarreal <hector.villarreal@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import models
class StockRule(models.Model):
_inherit = "stock.rule"
def _prepare_mo_vals(
self,
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
company_id,
values,
bom,
):
res = super()._prepare_mo_vals(
product_id,
product_qty,
product_uom,
location_id,
name,
origin,
company_id,
values,
bom,
)
if "planned_order_id" in values:
res["planned_order_id"] = values["planned_order_id"]
return res

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,9 @@
### MRP Areas
- Go to *Manufacturing \> Configuration \> MRP Areas* and define or edit
any existing area. You can specify the working hours for every area.
### Product MRP Area Parameters
- Go to *Manufacturing \> Products \> Product MRP Area Parameters*
and set the MRP parameters for a given product and area.

View File

@@ -0,0 +1,7 @@
- Wim Audenaert \<<wim.audenaert@ucamco.com>\>
- Jordi Ballester \<<jordi.ballester@forgeflow.com>\>
- Lois Rilo \<<lois.rilo@forgeflow.com>\>
- Héctor Villarreal \<<hector.villarreal@forgeflow.com>\>
- Christopher Ormaza \<<chris.ormaza@forgeflow.com>\>
- Joan Sisquella \<<joan.sisquella@forgeflow.com>\>
- Alexandre Fayolle \<<alexandre.fayolle@camptocamp.com>\>

View File

@@ -0,0 +1,19 @@
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 set by product variant MRP area pairs.
- Cron job to calculate the MRP demand.
- Manually calculate the MRP demand.
- Confirm the calculated MRP demand and create PO's, or MO's.
- Able to see the products for which action is needed throught Planned
Orders.
- Integration with [Stock Demand
Estimates](https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_demand_estimate)
system. Note: You need to install [mrp_multi_level_estimate
module](https://github.com/OCA/manufacture/tree/12.0/mrp_multi_level_estimate).

View File

@@ -0,0 +1,101 @@
## 13.0.1.5.0 (2020-04-09)
**Features**
- Show *Run MRP Multi Level* menu only to a specific new security group
*Run MRP Manually*.
([\#492](https://github.com/OCA/manufacture/issues/492))
## 13.0.1.4.0 (2020-03-26)
> - Add menu entry for planned orders
> - Add button to navigate from planned orders to linked manufacturing
> orders
> - Add action to convert planned orders to fixed
> - When changing the due date in a planned order the release date is
> recomputed
## 13.0.1.3.0 (2020-03-02)
- \[IMP\] Minor changes"
([\#470](https://github.com/OCA/manufacture/pull/470)).
- Planned Order release and due date become required.
- Add button to Product MRP Area to update MOQ from Supplier Info.
- Link Manufacturing Orders with Planned Orders.
- Allow Mrp Inventory Procure Wizard to be used from other models.
- Make MRP Inventory creation more extensible.
- Main Supplier computation (v13 requires explicit False definitions)
## 13.0.1.2.0 (2020-02-20)
- \[IMP\] Minor changes
([\#468](https://github.com/OCA/manufacture/pull/468)).
- Planned Orders become fixed on manual creation by default
- Released Quantity becomes readonly
- Add product reference if Planned Order name is not defined on bom
explosion
## 13.0.1.1.0 (2020-02-21)
- \[FIX\] Minor changes
([\#469](https://github.com/OCA/manufacture/pull/469)).
- Fix Main supplier computation in multi company
- Drop Triplicated field in search view
- \[IMP\] Minor changes
([\#463](https://github.com/OCA/manufacture/pull/463)).
- Show supply method on MRP Inventory
- Allow no-MRP users to look into Products
## 13.0.1.0.0 (2019-12-18)
- \[MIG\] Migration to v13.
## 12.0.1.0.0 (2019-08-05)
- \[MIG\] Migration to v12:
- Estimates as a forecasting mechanism is moved to a new module
(mrp_multi_level_estimate).
## 11.0.3.0.0 (2019-05-22)
- \[REW/IMP\] Rework to include Planned Orders.
([\#365](https://github.com/OCA/manufacture/pull/365)).
- \[IMP\] Able to procure from a different location than the area's
location.
## 11.0.2.2.0 (2019-05-02)
- \[IMP\] Able to run MRP only for selected areas.
([\#360](https://github.com/OCA/manufacture/pull/360)).
## 11.0.2.1.0 (2019-04-02)
- \[IMP\] Implement *Nbr. Days* functionality to be able to group demand
when generating supply proposals.
([\#345](https://github.com/OCA/manufacture/pull/345)).
## 11.0.2.0.0 (2018-11-20)
- \[REW\] Refactor MRP Area.
([\#322](https://github.com/OCA/manufacture/pull/322)):
- MRP product concept dropped in favor of *Product MRP Area
Parameters*. This allow to set different MRP parameters for the same
product in different areas.
- Menu items reordering.
## 11.0.1.1.0 (2018-08-30)
- \[FIX\] Consider *Qty Multiple* on product to propose the quantity to
procure. ([\#297](https://github.com/OCA/manufacture/pull/297))
## 11.0.1.0.1 (2018-08-03)
- \[FIX\] User and system locales doesn't break MRP calculation.
([\#290](https://github.com/OCA/manufacture/pull/290))
- \[FIX\] Working Hours are now defined only at Warehouse level and
displayed as a related on MRP Areas.
([\#290](https://github.com/OCA/manufacture/pull/290))
## 11.0.1.0.0 (2018-07-09)
- Start of the history.

View File

@@ -0,0 +1,12 @@
To manually run the MRP scheduler:
1. Go to *Manufacturing \> Planning \> Run MRP Multi Level*.
2. On the wizard click *Run MRP*.
To launch replenishment orders (moves, purchases, production orders...):
1. Go to *Manufacturing \> Planning \> MRP Inventory*.
2. Filter with *To procure*.
3. Select multiple records and click on *Action \> Procure* or click
the right hand side gears in any record.
4. On the wizard, check everything is ok and click *Execute*.

View File

@@ -0,0 +1,16 @@
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_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
access_product_mrp_area_user,product.mrp.area user,model_product_mrp_area,mrp.group_mrp_user,1,1,1,0
access_product_mrp_area_manager,product.mrp.area manager,model_product_mrp_area,mrp.group_mrp_manager,1,1,1,1
access_product_mrp_area_read,product.mrp.area read,model_product_mrp_area,base.group_user,1,0,0,0
access_mrp_planned_order_user,mrp.planned.order user,model_mrp_planned_order,mrp.group_mrp_user,1,0,0,0
access_mrp_planned_order_manager,mrp.planned.order manager,model_mrp_planned_order,mrp.group_mrp_manager,1,1,1,1
access_mrp_multi_level_user,mrp.multi.level user,model_mrp_multi_level,mrp.group_mrp_user,1,0,0,0
access_mrp_multi_level_manager,mrp.multi.level manager,model_mrp_multi_level,mrp.group_mrp_manager,1,1,1,1
access_mrp_inventory_procure_user,mrp.inventory.procure user,model_mrp_inventory_procure,mrp.group_mrp_user,1,1,1,1
access_mrp_inventory_procure_item_user,mrp.inventory.procure.item user,model_mrp_inventory_procure_item,mrp.group_mrp_user,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_area_user mrp.area user model_mrp_area mrp.group_mrp_user 1 0 0 0
7 access_mrp_area_manager mrp.area manager model_mrp_area mrp.group_mrp_manager 1 1 1 1
8 access_product_mrp_area_user product.mrp.area user model_product_mrp_area mrp.group_mrp_user 1 1 1 0
9 access_product_mrp_area_manager product.mrp.area manager model_product_mrp_area mrp.group_mrp_manager 1 1 1 1
10 access_product_mrp_area_read product.mrp.area read model_product_mrp_area base.group_user 1 0 0 0
11 access_mrp_planned_order_user mrp.planned.order user model_mrp_planned_order mrp.group_mrp_user 1 0 0 0
12 access_mrp_planned_order_manager mrp.planned.order manager model_mrp_planned_order mrp.group_mrp_manager 1 1 1 1
13 access_mrp_multi_level_user mrp.multi.level user model_mrp_multi_level mrp.group_mrp_user 1 0 0 0
14 access_mrp_multi_level_manager mrp.multi.level manager model_mrp_multi_level mrp.group_mrp_manager 1 1 1 1
15 access_mrp_inventory_procure_user mrp.inventory.procure user model_mrp_inventory_procure mrp.group_mrp_user 1 1 1 1
16 access_mrp_inventory_procure_item_user mrp.inventory.procure.item user model_mrp_inventory_procure_item mrp.group_mrp_user 1 1 1 1

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8" ?>
<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>
<record id="group_mrp_multi_level_run" model="res.groups">
<field name="name">Run MRP Manually</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
<field name="category_id" ref="base.module_category_hidden" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<record id="mrp_area_comp_rule" model="ir.rule">
<field name="name">MRP Area multi-company rule</field>
<field name="model_id" ref="model_mrp_area" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="mrp_product_mrp_area_comp_rule" model="ir.rule">
<field name="name">Product MRP Area multi-company rule</field>
<field name="model_id" ref="model_product_mrp_area" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="mrp_inventory_comp_rule" model="ir.rule">
<field name="name">MRP Inventory multi-company rule</field>
<field name="model_id" ref="model_mrp_inventory" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="mrp_move_comp_rule" model="ir.rule">
<field name="name">MRP Move multi-company rule</field>
<field name="model_id" ref="model_mrp_move" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
<record id="mrp_planned_order_comp_rule" model="ir.rule">
<field name="name">MRP Planned Order multi-company rule</field>
<field name="model_id" ref="model_mrp_planned_order" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,631 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>MRP Multi Level</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="mrp-multi-level">
<h1 class="title">MRP Multi Level</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:afddaf576893642eae579ee762efefb3c3ecc9f84445b23b43ab6fc448975fce
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/manufacture/tree/18.0/mrp_multi_level"><img alt="OCA/manufacture" src="https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/manufacture-18-0/manufacture-18-0-mrp_multi_level"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/manufacture&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>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.</p>
<p>To do this, the calculation starts at top level of the bill of material
and explodes this down to the lowest level.</p>
<div class="section" id="key-features">
<h1>Key Features</h1>
<ul class="simple">
<li>MRP parameters set by product variant MRP area pairs.</li>
<li>Cron job to calculate the MRP demand.</li>
<li>Manually calculate the MRP demand.</li>
<li>Confirm the calculated MRP demand and create POs, or MOs.</li>
<li>Able to see the products for which action is needed throught Planned
Orders.</li>
<li>Integration with <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_demand_estimate">Stock Demand
Estimates</a>
system. Note: You need to install <a class="reference external" href="https://github.com/OCA/manufacture/tree/12.0/mrp_multi_level_estimate">mrp_multi_level_estimate
module</a>.</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a><ul>
<li><a class="reference internal" href="#mrp-areas" id="toc-entry-2">MRP Areas</a></li>
<li><a class="reference internal" href="#product-mrp-area-parameters" id="toc-entry-3">Product MRP Area Parameters</a></li>
</ul>
</li>
<li><a class="reference internal" href="#usage" id="toc-entry-4">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-5">Changelog</a></li>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<div class="section" id="mrp-areas">
<h3><a class="toc-backref" href="#toc-entry-2">MRP Areas</a></h3>
<ul class="simple">
<li>Go to <em>Manufacturing &gt; Configuration &gt; MRP Areas</em> and define or edit
any existing area. You can specify the working hours for every area.</li>
</ul>
</div>
<div class="section" id="product-mrp-area-parameters">
<h3><a class="toc-backref" href="#toc-entry-3">Product MRP Area Parameters</a></h3>
<ul class="simple">
<li>Go to <em>Manufacturing &gt; Products &gt; Product MRP Area Parameters</em> and
set the MRP parameters for a given product and area.</li>
</ul>
</div>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-4">Usage</a></h2>
<p>To manually run the MRP scheduler:</p>
<ol class="arabic simple">
<li>Go to <em>Manufacturing &gt; Planning &gt; Run MRP Multi Level</em>.</li>
<li>On the wizard click <em>Run MRP</em>.</li>
</ol>
<p>To launch replenishment orders (moves, purchases, production orders…):</p>
<ol class="arabic simple">
<li>Go to <em>Manufacturing &gt; Planning &gt; MRP Inventory</em>.</li>
<li>Filter with <em>To procure</em>.</li>
<li>Select multiple records and click on <em>Action &gt; Procure</em> or click the
right hand side gears in any record.</li>
<li>On the wizard, check everything is ok and click <em>Execute</em>.</li>
</ol>
</div>
<div class="section" id="changelog">
<h2><a class="toc-backref" href="#toc-entry-5">Changelog</a></h2>
</div>
</div>
<div class="section" id="section-1">
<h1>13.0.1.5.0 (2020-04-09)</h1>
<p><strong>Features</strong></p>
<ul class="simple">
<li>Show <em>Run MRP Multi Level</em> menu only to a specific new security group
<em>Run MRP Manually</em>.
(<a class="reference external" href="https://github.com/OCA/manufacture/issues/492">#492</a>)</li>
</ul>
</div>
<div class="section" id="section-2">
<h1>13.0.1.4.0 (2020-03-26)</h1>
<blockquote>
<ul class="simple">
<li>Add menu entry for planned orders</li>
<li>Add button to navigate from planned orders to linked manufacturing
orders</li>
<li>Add action to convert planned orders to fixed</li>
<li>When changing the due date in a planned order the release date is
recomputed</li>
</ul>
</blockquote>
</div>
<div class="section" id="section-3">
<h1>13.0.1.3.0 (2020-03-02)</h1>
<ul class="simple">
<li>[IMP] Minor changes”
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/470">#470</a>).<ul>
<li>Planned Order release and due date become required.</li>
<li>Add button to Product MRP Area to update MOQ from Supplier Info.</li>
<li>Link Manufacturing Orders with Planned Orders.</li>
<li>Allow Mrp Inventory Procure Wizard to be used from other models.</li>
<li>Make MRP Inventory creation more extensible.</li>
<li>Main Supplier computation (v13 requires explicit False
definitions)</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="section-4">
<h1>13.0.1.2.0 (2020-02-20)</h1>
<ul class="simple">
<li>[IMP] Minor changes
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/468">#468</a>).<ul>
<li>Planned Orders become fixed on manual creation by default</li>
<li>Released Quantity becomes readonly</li>
<li>Add product reference if Planned Order name is not defined on bom
explosion</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="section-5">
<h1>13.0.1.1.0 (2020-02-21)</h1>
<ul class="simple">
<li>[FIX] Minor changes
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/469">#469</a>).<ul>
<li>Fix Main supplier computation in multi company</li>
<li>Drop Triplicated field in search view</li>
</ul>
</li>
<li>[IMP] Minor changes
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/463">#463</a>).<ul>
<li>Show supply method on MRP Inventory</li>
<li>Allow no-MRP users to look into Products</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="section-6">
<h1>13.0.1.0.0 (2019-12-18)</h1>
<ul class="simple">
<li>[MIG] Migration to v13.</li>
</ul>
</div>
<div class="section" id="section-7">
<h1>12.0.1.0.0 (2019-08-05)</h1>
<ul class="simple">
<li>[MIG] Migration to v12:<ul>
<li>Estimates as a forecasting mechanism is moved to a new module
(mrp_multi_level_estimate).</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="section-8">
<h1>11.0.3.0.0 (2019-05-22)</h1>
<ul class="simple">
<li>[REW/IMP] Rework to include Planned Orders.
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/365">#365</a>).</li>
<li>[IMP] Able to procure from a different location than the areas
location.</li>
</ul>
</div>
<div class="section" id="section-9">
<h1>11.0.2.2.0 (2019-05-02)</h1>
<ul class="simple">
<li>[IMP] Able to run MRP only for selected areas.
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/360">#360</a>).</li>
</ul>
</div>
<div class="section" id="section-10">
<h1>11.0.2.1.0 (2019-04-02)</h1>
<ul class="simple">
<li>[IMP] Implement <em>Nbr. Days</em> functionality to be able to group demand
when generating supply proposals.
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/345">#345</a>).</li>
</ul>
</div>
<div class="section" id="section-11">
<h1>11.0.2.0.0 (2018-11-20)</h1>
<ul class="simple">
<li>[REW] Refactor MRP Area.
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/322">#322</a>):<ul>
<li>MRP product concept dropped in favor of <em>Product MRP Area
Parameters</em>. This allow to set different MRP parameters for the
same product in different areas.</li>
<li>Menu items reordering.</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="section-12">
<h1>11.0.1.1.0 (2018-08-30)</h1>
<ul class="simple">
<li>[FIX] Consider <em>Qty Multiple</em> on product to propose the quantity to
procure. (<a class="reference external" href="https://github.com/OCA/manufacture/pull/297">#297</a>)</li>
</ul>
</div>
<div class="section" id="section-13">
<h1>11.0.1.0.1 (2018-08-03)</h1>
<ul class="simple">
<li>[FIX] User and system locales doesnt break MRP calculation.
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/290">#290</a>)</li>
<li>[FIX] Working Hours are now defined only at Warehouse level and
displayed as a related on MRP Areas.
(<a class="reference external" href="https://github.com/OCA/manufacture/pull/290">#290</a>)</li>
</ul>
</div>
<div class="section" id="section-14">
<h1>11.0.1.0.0 (2018-07-09)</h1>
<ul class="simple">
<li>Start of the history.</li>
</ul>
<div class="section" id="bug-tracker">
<h2>Bug Tracker</h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/manufacture/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_multi_level%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2>Credits</h2>
</div>
</div>
<div class="section" id="authors">
<h1>Authors</h1>
<ul class="simple">
<li>Ucamco</li>
<li>ForgeFlow</li>
</ul>
</div>
<div class="section" id="contributors">
<h1>Contributors</h1>
<ul class="simple">
<li>Wim Audenaert &lt;<a class="reference external" href="mailto:wim.audenaert&#64;ucamco.com">wim.audenaert&#64;ucamco.com</a>&gt;</li>
<li>Jordi Ballester &lt;<a class="reference external" href="mailto:jordi.ballester&#64;forgeflow.com">jordi.ballester&#64;forgeflow.com</a>&gt;</li>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
<li>Héctor Villarreal &lt;<a class="reference external" href="mailto:hector.villarreal&#64;forgeflow.com">hector.villarreal&#64;forgeflow.com</a>&gt;</li>
<li>Christopher Ormaza &lt;<a class="reference external" href="mailto:chris.ormaza&#64;forgeflow.com">chris.ormaza&#64;forgeflow.com</a>&gt;</li>
<li>Joan Sisquella &lt;<a class="reference external" href="mailto:joan.sisquella&#64;forgeflow.com">joan.sisquella&#64;forgeflow.com</a>&gt;</li>
<li>Alexandre Fayolle &lt;<a class="reference external" href="mailto:alexandre.fayolle&#64;camptocamp.com">alexandre.fayolle&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h1>Maintainers</h1>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/JordiBForgeFlow"><img alt="JordiBForgeFlow" src="https://github.com/JordiBForgeFlow.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/LoisRForgeFlow"><img alt="LoisRForgeFlow" src="https://github.com/LoisRForgeFlow.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/manufacture/tree/18.0/mrp_multi_level">OCA/manufacture</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1 @@
from . import test_mrp_multi_level

View File

@@ -0,0 +1,656 @@
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from datetime import datetime, timedelta
from odoo.tests import Form
from odoo.tests.common import TransactionCase
class TestMrpMultiLevelCommon(TransactionCase):
@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.quant_obj = cls.env["stock.quant"]
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.lot_obj = cls.env["stock.lot"]
cls.mrp_bom_obj = cls.env["mrp.bom"]
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.fp_3 = cls.env.ref("mrp_multi_level.product_product_fp_3")
cls.fp_4 = cls.env.ref("mrp_multi_level.product_product_fp_4")
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.sf_3 = cls.env.ref("mrp_multi_level.product_product_sf_3")
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.pp_3 = cls.env.ref("mrp_multi_level.product_product_pp_3")
cls.pp_4 = cls.env.ref("mrp_multi_level.product_product_pp_4")
cls.product_4b = cls.env.ref("product.product_product_4b")
cls.product_4c = cls.env.ref("product.product_product_4c")
cls.av_11 = cls.env.ref("mrp_multi_level.product_product_av_11")
cls.av_12 = cls.env.ref("mrp_multi_level.product_product_av_12")
cls.av_21 = cls.env.ref("mrp_multi_level.product_product_av_21")
cls.av_22 = cls.env.ref("mrp_multi_level.product_product_av_22")
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")
# Add calendar to WH:
cls.wh.calendar_id = cls.calendar
# Partner:
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")
cls.mrp_manager = cls._create_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}
)
# 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,
}
)
# Create products:
route_buy = cls.env.ref("purchase_stock.route_warehouse0_buy").id
cls.prod_test = cls.product_obj.create(
{
"name": "Test Top Seller",
"is_storable": True,
"list_price": 150.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": 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",
"is_storable": True,
"list_price": 50.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": 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",
"is_storable": True,
"list_price": 50.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": 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",
"is_storable": True,
"list_price": 50.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": 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",
"is_storable": True,
"list_price": 100.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": 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,
}
)
# Another product:
cls.product_tz = cls.product_obj.create(
{
"name": "Product Timezone",
"is_storable": True,
"list_price": 100.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": vendor1.id, "price": 20.0})],
}
)
cls.product_mrp_area_obj.create(
{"product_id": cls.product_tz.id, "mrp_area_id": cls.cases_area.id}
)
# Product to test special case with Purchase Uom:
cls.prod_uom_test = cls.product_obj.create(
{
"name": "Product Uom Test",
"is_storable": True,
"uom_id": cls.env.ref("uom.product_uom_unit").id,
"uom_po_id": cls.env.ref("uom.product_uom_dozen").id,
"list_price": 150.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": vendor1.id, "price": 20.0})],
}
)
cls.product_mrp_area_obj.create(
{"product_id": cls.prod_uom_test.id, "mrp_area_id": cls.mrp_area.id}
)
# Product to test lots
cls.product_lots = cls.product_obj.create(
{
"name": "Product Tracked by Lots",
"is_storable": True,
"tracking": "lot",
"uom_id": cls.env.ref("uom.product_uom_unit").id,
"list_price": 100.0,
"route_ids": [(6, 0, [route_buy])],
"seller_ids": [(0, 0, {"partner_id": vendor1.id, "price": 25.0})],
}
)
cls.product_mrp_area_obj.create(
{"product_id": cls.product_lots.id, "mrp_area_id": cls.mrp_area.id}
)
cls.lot_1 = cls.lot_obj.create(
{
"product_id": cls.product_lots.id,
"name": "Lot 1",
"company_id": cls.company.id,
}
)
cls.lot_2 = cls.lot_obj.create(
{
"product_id": cls.product_lots.id,
"name": "Lot 2",
"company_id": cls.company.id,
}
)
cls.quant_obj.sudo().create(
{
"product_id": cls.product_lots.id,
"lot_id": cls.lot_1.id,
"quantity": 100.0,
"location_id": cls.stock_location.id,
}
)
cls.quant_obj.sudo().create(
{
"product_id": cls.product_lots.id,
"lot_id": cls.lot_2.id,
"quantity": 110.0,
"location_id": cls.stock_location.id,
}
)
# Product MRP Parameter to test supply method computation
cls.env.ref("stock.route_warehouse0_mto").active = True
cls.env["stock.rule"].create(
{
"name": "WH2: Main Area → Secondary Area (MTO)",
"action": "pull",
"picking_type_id": cls.env.ref("stock.picking_type_in").id,
"location_src_id": cls.env.ref("stock.stock_location_stock").id,
"location_dest_id": cls.sec_loc.id,
"route_id": cls.env.ref("stock.route_warehouse0_mto").id,
"procure_method": "mts_else_mto",
}
)
cls.product_mrp_area_obj.create(
{"product_id": cls.fp_4.id, "mrp_area_id": cls.secondary_area.id}
)
# 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
)
dt_bit_later = dt_base + timedelta(hours=1)
cls._create_picking_out(
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
)
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
)
# product_4b will use the template bom (sequence 5)
# (11, 22) = ("steel", "black")
# create variant bom for product_4c (sequence 1)
# (12, 21) = ("aluminum", "white")
cls.mrp_bom_obj.create(
{
"product_tmpl_id": cls.product_4c.product_tmpl_id.id,
"product_id": cls.product_4c.id,
"type": "normal",
"sequence": 1,
"bom_line_ids": [
(
0,
0,
{
"product_id": cls.av_12.id,
"product_qty": 1.0,
},
),
(
0,
0,
{
"product_id": cls.av_21.id,
"product_qty": 1.0,
},
),
],
}
)
# Create test picking for FP-1, FP-2, Desk(steel, black), Desk(aluminum, white)
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,
"scheduled_date": date_move,
"move_ids": [
(
0,
0,
{
"name": "Test move fp-1",
"product_id": cls.fp_1.id,
"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": 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,
},
),
(
0,
0,
{
"name": "Test move fp-3",
"product_id": cls.fp_3.id,
"date": date_move,
"product_uom": cls.fp_3.uom_id.id,
"product_uom_qty": 5,
"location_id": cls.stock_location.id,
"location_dest_id": cls.customer_location.id,
},
),
(
0,
0,
{
"name": "Test move product-4b",
"product_id": cls.product_4b.id,
"date": date_move,
"product_uom": cls.product_4b.uom_id.id,
"product_uom_qty": 150,
"location_id": cls.stock_location.id,
"location_dest_id": cls.customer_location.id,
},
),
(
0,
0,
{
"name": "Test move product-4c",
"product_id": cls.product_4c.id,
"date": date_move,
"product_uom": cls.product_4c.uom_id.id,
"product_uom_qty": 56,
"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,
"scheduled_date": date_move,
"move_ids": [
(
0,
0,
{
"name": "Test move prod_min",
"product_id": cls.prod_min.id,
"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": 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": 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,
},
)
],
}
)
# Create Test PO for special case Puchase uom:
# Remember that prod_uom_test had a UoM of units but it is purchased in dozens.
# For this reason buying 1 quantity of it, means to have 12 units in stock.
date_po = cls.calendar.plan_days(1 + 1, datetime.today().replace(hour=0)).date()
cls.po_uom = cls.po_obj.create(
{
"name": "Test PO-002",
"partner_id": cls.vendor.id,
"order_line": [
(
0,
0,
{
"name": "Product Uom Test line",
"product_id": cls.prod_uom_test.id,
"date_planned": date_po,
"product_qty": 1.0,
"product_uom": cls.prod_uom_test.uom_po_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._create_mo(cls.fp_2, bom_fp_2, date_mo, qty=12.0)
# Dates:
today = datetime.today().replace(hour=0)
cls.date_3 = cls.calendar.plan_days(3 + 1, today).date()
cls.date_5 = cls.calendar.plan_days(5 + 1, today).date()
cls.date_6 = cls.calendar.plan_days(6 + 1, today).date()
cls.date_7 = cls.calendar.plan_days(7 + 1, today).date()
cls.date_8 = cls.calendar.plan_days(8 + 1, today).date()
cls.date_9 = cls.calendar.plan_days(9 + 1, today).date()
cls.date_10 = cls.calendar.plan_days(10 + 1, today).date()
cls.date_20 = cls.calendar.plan_days(20 + 1, today).date()
cls.date_22 = cls.calendar.plan_days(22 + 1, today).date()
# Create movements in secondary area:
cls.create_demand_sec_loc(cls.date_8, 80.0)
cls.create_demand_sec_loc(cls.date_9, 50.0)
cls.create_demand_sec_loc(cls.date_10, 70.0)
cls.create_demand_sec_loc(cls.date_20, 46.0)
cls.create_demand_sec_loc(cls.date_22, 33.0)
# Create pickings:
cls._create_picking_out(cls.product_lots, 25, today)
cls.mrp_multi_level_wiz.create({}).run_mrp_multi_level()
@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,
"scheduled_date": date_move,
"move_ids": [
(
0,
0,
{
"name": "Test move",
"product_id": cls.prod_test.id,
"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])],
}
)
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,
"scheduled_date": date_move,
"move_ids": [
(
0,
0,
{
"name": "Test Move",
"product_id": product.id,
"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
@classmethod
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,
"scheduled_date": date_move,
"move_ids": [
(
0,
0,
{
"name": "Test Move",
"product_id": product.id,
"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
@classmethod
def _create_mo(cls, product, bom, date, qty=10.0):
mo_form = Form(cls.mo_obj)
mo_form.product_id = product
mo_form.bom_id = bom
mo_form.product_qty = qty
mo_form.date_start = date
mo = mo_form.save()
# Confirm the MO to generate stock moves:
mo.action_confirm()
return mo

View File

@@ -0,0 +1,930 @@
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from datetime import date, datetime, timedelta
from odoo import fields
from .common import TestMrpMultiLevelCommon
class TestMrpMultiLevel(TestMrpMultiLevelCommon):
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_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")
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")
self.assertFalse(product_mrp_area.main_supplier_id)
self.assertFalse(product_mrp_area.main_supplierinfo_id)
# Archiving the product should archive parameters:
self.assertTrue(product_mrp_area.active)
self.sf_1.active = False
self.assertFalse(product_mrp_area.active)
def test_03_mrp_moves(self):
"""Tests for mrp moves generated."""
moves = self.mrp_move_obj.search([("product_id", "=", self.pp_1.id)])
self.assertEqual(len(moves), 3)
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:
# Demand coming from FP-1
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:
# Demand coming from FP-2 -> SF-1
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)]
)
self.assertEqual(len(planned_orders), 3)
for plan in planned_orders:
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")]
)
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(
[("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),
]
)
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)
fp_2_line_2 = self.mrp_inventory_obj.search(
[
("product_mrp_area_id.product_id", "=", self.fp_2.id),
("date", "=", self.date_10),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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),
]
)
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_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"
)
self.assertEqual(len(invs), 2)
expected = [0.0, 0.0] # No grouping, lot size nor safety stock.
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)])
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)])
self.assertTrue(mos)
self.assertEqual(mos.product_qty, 100.0)
mo_date_start = fields.Date.to_date(mos.date_start)
self.assertEqual(mo_date_start, self.date_5)
def test_07_adjust_qty_to_order(self):
"""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)]
)
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)]
)
self.assertEqual(mrp_inv_max.to_procure, 150)
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"))
# quantity multiple:
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),
]
)
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")
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"))
self.assertIn(abs(week_1_expected), quantities)
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.with_user(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
)
self.assertNotEqual(this.create_uid, prev.create_uid)
def test_10_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)]
)
self.assertEqual(len(moves), 4)
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)
# Net needs = 18, available on-hand = 3 -> 15
self.assertEqual(mrp_invs[1].to_procure, 15)
def test_11_bom_line_attribute_value_skip(self):
"""Check for the correct demand on components of a product with
multiple variants"""
product_4b_demand = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.product_4b.id)]
)
self.assertTrue(product_4b_demand)
self.assertEqual(product_4b_demand.to_procure, 100)
product_4c_demand = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.product_4c.id)]
)
self.assertTrue(product_4c_demand)
self.assertEqual(product_4c_demand.to_procure, 1)
# Testing variant BoM
# Supply of one unit for AV-12 or AV-21
av_12_supply = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.av_12.id)]
)
self.assertEqual(av_12_supply.to_procure, 1.0)
av_21_supply = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.av_21.id)]
)
self.assertEqual(av_21_supply.to_procure, 1.0)
# Testing template BoM
# Supply of 150 units for AV-11 and AV-22
av_11_supply = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.av_11.id)]
)
self.assertEqual(av_11_supply.to_procure, 100.0)
av_22_supply = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.av_22.id)]
)
self.assertTrue(av_22_supply.to_procure, 100.0)
def test_12_timezone_handling(self):
self.calendar.tz = "Australia/Sydney" # Oct-Apr/Apr-Oct: UTC+11/UTC+10
date_move = datetime(2090, 4, 19, 20, 00) # Apr 20 6/7 am in Sidney
sidney_date = date(2090, 4, 20)
self._create_picking_in(
self.product_tz, 10.0, date_move, location=self.cases_loc
)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[
("mrp_area_id", "=", self.cases_area.id),
("product_id", "=", self.product_tz.id),
]
)
self.assertEqual(len(inventory), 1)
self.assertEqual(inventory.date, sidney_date)
def test_13_timezone_not_set(self):
self.wh.calendar_id = False
date_move = datetime(2090, 4, 19, 20, 00)
self._create_picking_in(
self.product_tz, 10.0, date_move, location=self.cases_loc
)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[
("mrp_area_id", "=", self.cases_area.id),
("product_id", "=", self.product_tz.id),
]
)
self.assertEqual(len(inventory), 1)
self.assertEqual(inventory.date, date_move.date())
def test_14_units_case(self):
"""When a product has a different purchase unit of measure than
the general unit of measure and the supply is coming from an RFQ"""
prod_uom_test_inventory_lines = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.prod_uom_test.id)]
)
self.assertEqual(len(prod_uom_test_inventory_lines), 1)
self.assertEqual(prod_uom_test_inventory_lines.supply_qty, 12.0)
# Supply qty has to be 12 has a dozen of units are in a RFQ.
def test_15_phantom_comp_planning(self):
"""
Phantom components will not appear in MRP Inventory or Planned Orders.
MRP Parameter will have 'phantom' supply method.
"""
# SF-3
sf_3_line_1 = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.sf_3.id)]
)
self.assertEqual(len(sf_3_line_1), 0)
sf_3_planned_order_1 = self.planned_order_obj.search(
[("product_mrp_area_id.product_id", "=", self.sf_3.id)]
)
self.assertEqual(sf_3_planned_order_1.mrp_action, "phantom")
self.assertEqual(sf_3_planned_order_1.mrp_qty, 10.0)
# PP-3
pp_3_line_1 = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
)
self.assertEqual(len(pp_3_line_1), 1)
self.assertEqual(pp_3_line_1.demand_qty, 20.0)
pp_3_planned_orders = self.planned_order_obj.search(
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
)
self.assertEqual(len(pp_3_planned_orders), 2)
# PP-4
pp_4_line_1 = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.pp_4.id)]
)
self.assertEqual(len(pp_4_line_1), 1)
self.assertEqual(pp_4_line_1.demand_qty, 30.0)
pp_4_planned_orders = self.planned_order_obj.search(
[("product_mrp_area_id.product_id", "=", self.pp_4.id)]
)
self.assertEqual(len(pp_4_planned_orders), 1)
def test_16_supply_method(self):
"""Test supply method computation."""
self.fp_4.route_ids = [(5, 0, 0)]
product_mrp_area = self.product_mrp_area_obj.search(
[("product_id", "=", self.fp_4.id)]
)
self.assertEqual(product_mrp_area.supply_method, "none")
self.fp_4.route_ids = [(4, self.env.ref("stock.route_warehouse0_mto").id)]
product_mrp_area._compute_supply_method()
self.assertEqual(product_mrp_area.supply_method, "pull")
self.fp_4.route_ids = [(4, self.env.ref("mrp.route_warehouse0_manufacture").id)]
product_mrp_area._compute_supply_method()
self.assertEqual(product_mrp_area.supply_method, "manufacture")
# because of the issue discussed here https://github.com/odoo/odoo/pull/188846
# we need to apply routes explicitly in the proper order (by sequence)
self.fp_4.route_ids = [
(
6,
0,
(
self.env.ref("stock.route_warehouse0_mto")
+ self.env.ref("purchase_stock.route_warehouse0_buy")
+ self.env.ref("mrp.route_warehouse0_manufacture")
).ids,
)
]
product_mrp_area._compute_supply_method()
self.assertEqual(product_mrp_area.supply_method, "buy")
kit_bom = self.mrp_bom_obj.create(
{
"product_tmpl_id": self.fp_4.product_tmpl_id.id,
"product_id": self.fp_4.id,
"type": "phantom",
}
)
product_mrp_area._compute_supply_method()
self.assertEqual(product_mrp_area.supply_method, "phantom")
self.assertEqual(product_mrp_area.supply_bom_id, kit_bom)
def test_17_priorize_safety_stock(self):
now = datetime.now()
product = self.prod_test # has Buy route
product.seller_ids[0].delay = 2 # set a purchase lead time
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
self.product_mrp_area_obj.create(
{
"product_id": product.id,
"mrp_area_id": self.cases_area.id,
"mrp_minimum_stock": 15,
"mrp_applicable": True, # needed?
}
)
self._create_picking_out(
product, 6.0, now + timedelta(days=3), location=self.cases_loc
)
self._create_picking_in(
product, 10.0, now + timedelta(days=7), location=self.cases_loc
)
self._create_picking_out(
product, 12.0, now + timedelta(days=14), location=self.cases_loc
)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[
("mrp_area_id", "=", self.cases_area.id),
("product_id", "=", product.id),
]
)
expected = [
{
"date": now.date(),
"demand_qty": 0.0,
"final_on_hand_qty": 5.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 10.0,
},
{
"date": now.date() + timedelta(days=3),
"demand_qty": 6.0,
"final_on_hand_qty": -1.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 6.0,
},
{
"date": now.date() + timedelta(days=7),
"demand_qty": 0.0,
"final_on_hand_qty": 9.0,
"initial_on_hand_qty": -1.0,
"running_availability": 25.0,
"supply_qty": 10.0,
"to_procure": 0.0,
},
{
"date": now.date() + timedelta(days=14),
"demand_qty": 12.0,
"final_on_hand_qty": -3.0,
"initial_on_hand_qty": 9.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 2.0,
},
]
self.assertEqual(len(expected), len(inventory))
for test_vals, inv in zip(expected, inventory, strict=True):
for key in test_vals:
self.assertEqual(
test_vals[key],
inv[key],
f"unexpected value for {key}: {inv[key]} "
f"(expected {test_vals[key]} on {inv.date})",
)
def test_18_on_hand_with_lots(self):
"""Check that on-hand is correctly computed when tracking by lots."""
lots_line_1 = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.product_lots.id)]
)
self.assertEqual(len(lots_line_1), 1)
self.assertEqual(lots_line_1.initial_on_hand_qty, 210)
self.assertEqual(lots_line_1.final_on_hand_qty, 185)
def test_19_prioritize_safety_stock_grouped_1(self):
"""Test grouped demand MRP but with a short nbr days.
Safety stock should be ordered."""
now = datetime.now()
product = self.prod_test # has Buy route
product.seller_ids[0].delay = 2 # set a purchase lead time
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
self.product_mrp_area_obj.create(
{
"product_id": product.id,
"mrp_area_id": self.cases_area.id,
"mrp_minimum_stock": 15,
"mrp_nbr_days": 2,
}
)
self._create_picking_out(
product, 6.0, now + timedelta(days=3), location=self.cases_loc
)
self._create_picking_in(
product, 10.0, now + timedelta(days=7), location=self.cases_loc
)
self._create_picking_out(
product, 12.0, now + timedelta(days=14), location=self.cases_loc
)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[
("mrp_area_id", "=", self.cases_area.id),
("product_id", "=", product.id),
]
)
expected = [
{
"date": now.date(),
"demand_qty": 0.0,
"final_on_hand_qty": 5.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 10.0,
},
{
"date": now.date() + timedelta(days=3),
"demand_qty": 6.0,
"final_on_hand_qty": -1.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 6.0,
},
{
"date": now.date() + timedelta(days=7),
"demand_qty": 0.0,
"final_on_hand_qty": 9.0,
"initial_on_hand_qty": -1.0,
"running_availability": 25.0,
"supply_qty": 10.0,
"to_procure": 0.0,
},
{
"date": now.date() + timedelta(days=14),
"demand_qty": 12.0,
"final_on_hand_qty": -3.0,
"initial_on_hand_qty": 9.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 2.0,
},
]
self.assertEqual(len(expected), len(inventory))
for test_vals, inv in zip(expected, inventory, strict=True):
for key in test_vals:
self.assertEqual(
test_vals[key],
inv[key],
f"unexpected value for {key}: {inv[key]} "
f"(expected {test_vals[key]} on {inv.date})",
)
def test_20_prioritize_safety_stock_grouped_2(self):
"""Test grouped demand MRP but with a longer nbr days.
Safety stock should be ordered."""
now = datetime.now()
product = self.prod_test # has Buy route
product.seller_ids[0].delay = 2 # set a purchase lead time
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
self.product_mrp_area_obj.create(
{
"product_id": product.id,
"mrp_area_id": self.cases_area.id,
"mrp_minimum_stock": 15,
"mrp_nbr_days": 7,
}
)
self._create_picking_out(
product, 6.0, now + timedelta(days=3), location=self.cases_loc
)
self._create_picking_in(
product, 10.0, now + timedelta(days=7), location=self.cases_loc
)
self._create_picking_out(
product, 12.0, now + timedelta(days=12), location=self.cases_loc
)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[
("mrp_area_id", "=", self.cases_area.id),
("product_id", "=", product.id),
]
)
expected = [
{
"date": now.date(),
"demand_qty": 0.0,
"final_on_hand_qty": 5.0,
"initial_on_hand_qty": 5.0,
"running_availability": 21.0,
"supply_qty": 0.0,
"to_procure": 16.0,
},
{
"date": now.date() + timedelta(days=3),
"demand_qty": 6.0,
"final_on_hand_qty": -1.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 0.0,
},
{
"date": now.date() + timedelta(days=7),
"demand_qty": 0.0,
"final_on_hand_qty": 9.0,
"initial_on_hand_qty": -1.0,
"running_availability": 27.0,
"supply_qty": 10.0,
"to_procure": 2.0,
},
{
"date": now.date() + timedelta(days=12),
"demand_qty": 12.0,
"final_on_hand_qty": -3.0,
"initial_on_hand_qty": 9.0,
"running_availability": 15.0,
"supply_qty": 0.0,
"to_procure": 0.0,
},
]
self.assertEqual(len(expected), len(inventory))
for test_vals, inv in zip(expected, inventory, strict=True):
for key in test_vals:
self.assertEqual(
test_vals[key],
inv[key],
f"unexpected value for {key}: {inv[key]} "
f"(expected {test_vals[key]} on {inv.date})",
)
def test_21_prioritize_safety_stock_grouped_3(self):
"""Test grouped demand MRP but with an existing incoming supply
Safety stock should NOT be ordered."""
now = datetime.now()
product = self.prod_test # has Buy route
product.seller_ids[0].delay = 2 # set a purchase lead time
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
self.product_mrp_area_obj.create(
{
"product_id": product.id,
"mrp_area_id": self.cases_area.id,
"mrp_minimum_stock": 15,
"mrp_nbr_days": 7,
}
)
self._create_picking_in(
product, 30.0, now + timedelta(days=3), location=self.cases_loc
)
self._create_picking_out(
product, 6.0, now + timedelta(days=7), location=self.cases_loc
)
self._create_picking_out(
product, 12.0, now + timedelta(days=12), location=self.cases_loc
)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[
("mrp_area_id", "=", self.cases_area.id),
("product_id", "=", product.id),
]
)
expected = [
{
"date": now.date() + timedelta(days=3),
"demand_qty": 0.0,
"initial_on_hand_qty": 5.0,
"final_on_hand_qty": 35.0,
"running_availability": 35.0,
"supply_qty": 30.0,
"to_procure": 0.0,
},
{
"date": now.date() + timedelta(days=7),
"demand_qty": 6.0,
"initial_on_hand_qty": 35.0,
"final_on_hand_qty": 29.0,
"running_availability": 29.0,
"supply_qty": 0.0,
"to_procure": 0.0,
},
{
"date": now.date() + timedelta(days=12),
"demand_qty": 12.0,
"initial_on_hand_qty": 29.0,
"final_on_hand_qty": 17.0,
"running_availability": 17.0,
"supply_qty": 0.0,
"to_procure": 0.0,
},
]
self.assertEqual(len(expected), len(inventory))
for test_vals, inv in zip(expected, inventory, strict=True):
for key in test_vals:
self.assertEqual(
test_vals[key],
inv[key],
f"unexpected value for {key}: {inv[key]} "
f"(expected {test_vals[key]} on {inv.date})",
)
def test_22_prioritize_safety_stock_with_mrp_moves_today(self):
"""Test MRP but with moves today. Safety stock should not be ordered."""
now = datetime.now()
product = self.prod_test # has Buy route
product.seller_ids[0].delay = 2 # set a purchase lead time
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
self.product_mrp_area_obj.create(
{
"product_id": product.id,
"mrp_area_id": self.cases_area.id,
"mrp_minimum_stock": 15,
}
)
self._create_picking_out(product, 10.0, now, location=self.cases_loc)
self._create_picking_in(product, 20.0, now, location=self.cases_loc)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[("mrp_area_id", "=", self.cases_area.id), ("product_id", "=", product.id)]
)
expected = [
{
"date": now.date(),
"demand_qty": 10.0,
"final_on_hand_qty": 15.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 20.0,
"to_procure": 0.0,
},
]
self.assertEqual(len(expected), len(inventory))
for test_vals, inv in zip(expected, inventory, strict=True):
for key in test_vals:
self.assertEqual(
test_vals[key],
inv[key],
f"unexpected value for {key}: {inv[key]} "
f"(expected {test_vals[key]} on {inv.date})",
)
def test_23_prioritize_safety_stock_with_mrp_moves_today_grouped(self):
"""Test grouped demand MRP but with moves today. Safety stock should not be
ordered.
"""
now = datetime.now()
product = self.prod_test # has Buy route
product.seller_ids[0].delay = 2 # set a purchase lead time
self.quant_obj._update_available_quantity(product, self.cases_loc, 5)
self.product_mrp_area_obj.create(
{
"product_id": product.id,
"mrp_area_id": self.cases_area.id,
"mrp_minimum_stock": 15,
"mrp_nbr_days": 2,
}
)
self._create_picking_out(product, 10.0, now, location=self.cases_loc)
self._create_picking_in(product, 20.0, now, location=self.cases_loc)
self.mrp_multi_level_wiz.create(
{"mrp_area_ids": [(6, 0, self.cases_area.ids)]}
).run_mrp_multi_level()
inventory = self.mrp_inventory_obj.search(
[("mrp_area_id", "=", self.cases_area.id), ("product_id", "=", product.id)]
)
expected = [
{
"date": now.date(),
"demand_qty": 10.0,
"final_on_hand_qty": 15.0,
"initial_on_hand_qty": 5.0,
"running_availability": 15.0,
"supply_qty": 20.0,
"to_procure": 0.0,
},
]
self.assertEqual(len(expected), len(inventory))
for test_vals, inv in zip(expected, inventory, strict=True):
for key in test_vals:
self.assertEqual(
test_vals[key],
inv[key],
f"unexpected value for {key}: {inv[key]} "
f"(expected {test_vals[key]} on {inv.date})",
)
def test_24_phantom_comp_on_hand(self):
"""
A phantom product with positive qty_available (which is computed from the
availability of its components) should not satisfy demand, because this leads
to double counting qty_available of its component products.
"""
quant = self.quant_obj.sudo().create(
{
"product_id": self.pp_3.id,
"inventory_quantity": 10.0,
"location_id": self.stock_location.id,
}
)
quant.action_apply_inventory()
quant = self.quant_obj.sudo().create(
{
"product_id": self.pp_4.id,
"inventory_quantity": 30.0,
"location_id": self.stock_location.id,
}
)
quant.action_apply_inventory()
self.assertEqual(self.sf_3.qty_available, 10.0)
self.mrp_multi_level_wiz.create({}).run_mrp_multi_level()
# PP-3
pp_3_line_1 = self.mrp_inventory_obj.search(
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
)
self.assertEqual(len(pp_3_line_1), 1)
self.assertEqual(pp_3_line_1.demand_qty, 20.0)
self.assertEqual(pp_3_line_1.to_procure, 10.0)
pp_3_planned_orders = self.planned_order_obj.search(
[("product_mrp_area_id.product_id", "=", self.pp_3.id)]
)
self.assertEqual(len(pp_3_planned_orders), 1)
self.assertEqual(pp_3_planned_orders.mrp_qty, 10)
sf3_planned_orders = self.env["mrp.planned.order"].search(
[("product_id", "=", self.sf_3.id)]
)
self.assertEqual(len(sf3_planned_orders), 1)
# Trying to procure a kit planned order will have no effect.
procure_wizard = (
self.env["mrp.inventory.procure"]
.with_context(
active_model="mrp.planned.order", active_ids=sf3_planned_orders.ids
)
.create({})
)
self.assertEqual(len(procure_wizard.item_ids), 0)

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="mrp_area_list">
<field name="name">mrp.area.list</field>
<field name="model">mrp.area</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="warehouse_id" />
<field name="company_id" groups="base.group_multi_company" />
<field name="location_id" />
<field name="calendar_id" />
</list>
</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">
<sheet>
<div class="oe_button_box" name="button_box" />
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
invisible="active"
/>
<label for="name" class="oe_edit_only" />
<h1>
<field name="name" />
</h1>
<group colspan="4" col="2">
<group>
<field name="active" invisible="1" />
<field name="warehouse_id" />
<field name="location_id" />
<field
name="company_id"
groups="base.group_multi_company"
/>
</group>
<group name="settings">
<field name="calendar_id" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="mrp_area_action">
<field name="name">MRP Area</field>
<field name="res_model">mrp.area</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="mrp_area_list" />
</record>
</odoo>

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="mrp_inventory_form" model="ir.ui.view">
<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 Inventory" create="false" edit="false">
<sheet>
<group>
<group>
<field name="mrp_area_id" />
<field
name="company_id"
groups="base.group_multi_company"
/>
<field name="product_id" />
<field name="product_mrp_area_id" />
<field name="supply_method" />
<field
name="main_supplier_id"
invisible="supply_method != 'buy'"
/>
<field name="date" />
</group>
<group>
<field name="initial_on_hand_qty" />
<field name="demand_qty" />
<field name="supply_qty" />
<field name="final_on_hand_qty" />
<field name="to_procure" />
<field name="uom_id" groups="uom.group_uom" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="mrp_inventory_list" model="ir.ui.view">
<field name="name">mrp.inventory.list</field>
<field name="model">mrp.inventory</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list create="false">
<field name="mrp_area_id" />
<field name="company_id" groups="base.group_multi_company" />
<field name="product_id" />
<field name="date" />
<field name="uom_id" groups="uom.group_uom" />
<field name="initial_on_hand_qty" />
<field name="demand_qty" />
<field name="supply_qty" />
<field name="final_on_hand_qty" />
<field name="to_procure" />
<button
invisible="not planned_order_ids"
name="action_open_planned_orders"
type="object"
icon="fa-list"
title="Details"
/>
<field name="order_release_date" />
<button
title="Create Procurement"
name="%(mrp_multi_level.act_mrp_inventory_procure)d"
icon="fa-cogs"
type="action"
invisible="to_procure &lt;= 0.0"
/>
<field name="planned_order_ids" column_invisible="True" />
<field name="supply_method" />
<field name="main_supplier_id" optional="hide" />
<field name="running_availability" />
</list>
</field>
</record>
<record id="view_mrp_inventory_pivot" model="ir.ui.view">
<field name="name">mrp.inventory.pivot</field>
<field name="model">mrp.inventory</field>
<field name="arch" type="xml">
<pivot string="MRP Inventory">
<field name="final_on_hand_qty" type="measure" />
<field name="mrp_area_id" type="row" />
<field name="product_mrp_area_id" type="row" />
<field name="date" interval="day" type="col" />
</pivot>
</field>
</record>
<record id="view_mrp_inventory_graph" model="ir.ui.view">
<field name="name">mrp.inventory.graph</field>
<field name="model">mrp.inventory</field>
<field name="arch" type="xml">
<graph string="MRP Inventory" type="line">
<field name="final_on_hand_qty" type="measure" />
<field name="date" interval="day" type="row" />
<field name="product_mrp_area_id" type="row" />
</graph>
</field>
</record>
<record id="mrp_inventory_search" model="ir.ui.view">
<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="product_id" />
<field name="mrp_area_id" />
<field name="company_id" groups="base.group_multi_company" />
</group>
<separator />
<field name="mrp_planner_id" invisible="1" />
<filter
string="My products"
name="mrp_planner_id"
domain="[('mrp_planner_id', '=', uid)]"
/>
<filter
string="To Procure"
name="filter_to_procure"
domain="[['to_procure','>',0.0]]"
/>
<separator />
<group expand="0" string="Group By...">
<filter
name="group_product"
string="Product"
context="{'group_by':'product_mrp_area_id'}"
/>
<filter
name="group_mrp_area"
string="MRP Area"
context="{'group_by':'mrp_area_id'}"
/>
<filter
name="group_supply_method"
string="Supply Method"
context="{'group_by':'supply_method'}"
/>
<filter
name="group_main_supplier_id"
string="Main Supplier"
context="{'group_by':'main_supplier_id'}"
/>
<filter
name="group_date"
string="Date"
context="{'group_by':'date'}"
/>
<filter
name="group_release_date"
string="Date to Procure"
context="{'group_by':'order_release_date'}"
/>
</group>
</search>
</field>
</record>
<record id="mrp_inventory_action" model="ir.actions.act_window">
<field name="name">MRP Inventory</field>
<field name="res_model">mrp.inventory</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">list,form,pivot,graph</field>
<field name="view_id" ref="mrp_inventory_list" />
<field name="search_view_id" ref="mrp_inventory_search" />
</record>
</odoo>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8" ?>
<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="mrp.menu_mrp_configuration"
sequence="50"
/>
<menuitem
name="Product MRP Area Parameters"
id="menu_product_mrp_area_parameters"
action="product_mrp_area_action"
parent="mrp.menu_mrp_bom"
sequence="10"
/>
<menuitem
name="MRP Inventory"
id="menu_mrp_inventory"
action="mrp_inventory_action"
parent="mrp.mrp_planning_menu_root"
sequence="30"
/>
<menuitem
name="Run MRP Multi Level"
id="menu_mrp_multi_level"
action="action_mrp_multi_level"
parent="mrp.mrp_planning_menu_root"
groups="mrp_multi_level.group_mrp_multi_level_run"
sequence="40"
/>
</odoo>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="mrp_move_view_list" model="ir.ui.view">
<field name="name">mrp.move.list</field>
<field name="model">mrp.move</field>
<field name="arch" type="xml">
<list>
<field name="mrp_date" />
<field name="current_date" />
<field name="mrp_origin" />
<field name="state" />
<field name="mrp_order_number" />
<field name="parent_product_id" />
<field name="name" />
<field name="mrp_qty" />
<field name="current_qty" />
<field name="mrp_type" />
<field name="planned_order_up_ids" />
</list>
</field>
</record>
<record id="mrp_move_view_form" model="ir.ui.view">
<field name="name">mrp.move.form</field>
<field name="model">mrp.move</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="mrp_area_id" />
<field
name="company_id"
groups="base.group_multi_company"
/>
<field name="product_id" />
<field name="product_mrp_area_id" />
<field name="mrp_origin" />
<field
name="production_id"
invisible="mrp_origin != 'mo'"
/>
<field
name="purchase_order_id"
invisible="mrp_origin != 'po'"
/>
<field
name="purchase_line_id"
invisible="mrp_origin != 'po'"
/>
<field
name="stock_move_id"
invisible="mrp_origin != 'mv'"
/>
<field name="name" />
<field name="origin" />
</group>
<group>
<field name="mrp_date" />
<field name="current_date" invisible="not current_date" />
<field name="state" invisible="not state" />
<field
name="mrp_order_number"
invisible="not mrp_order_number"
/>
<field
name="parent_product_id"
invisible="not parent_product_id"
/>
<field name="mrp_qty" />
<field name="current_qty" />
<field name="mrp_type" />
</group>
</group>
<notebook>
<page string="Planned Orders UP" name="planned_orders_up">
<field name="planned_order_up_ids" readonly="1" />
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mrp_move_action" model="ir.actions.act_window">
<field name="name">MRP Moves</field>
<field name="res_model">mrp.move</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019-20 ForgeFlow S.L. (https://www.forgeflow.com)
Part of ForgeFlow. See LICENSE file for full copyright and licensing details. -->
<odoo>
<record id="mrp_planned_order_view_list" model="ir.ui.view">
<field name="name">mrp.planned.order.list</field>
<field name="model">mrp.planned.order</field>
<field name="arch" type="xml">
<list
decoration-info="fixed != True and mrp_action != 'phantom'"
decoration-muted="mrp_action == 'phantom'"
>
<field name="name" />
<field name="origin" />
<field name="product_mrp_area_id" />
<field name="product_id" />
<field name="mrp_area_id" />
<field name="order_release_date" />
<field name="due_date" />
<field name="qty_released" />
<field name="mrp_qty" />
<field name="fixed" />
<field name="mrp_action" optional="hide" />
</list>
</field>
</record>
<record id="mrp_planned_order_view_form" model="ir.ui.view">
<field name="name">mrp.planned.order.form</field>
<field name="model">mrp.planned.order</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_open_linked_mrp_production"
type="object"
icon="fa-wrench"
invisible="mo_count == 0"
class="oe_stat_button"
>
<field name="mo_count" widget="statinfo" string="MOs" />
</button>
</div>
<group>
<group>
<field name="product_mrp_area_id" />
<field name="product_id" />
<field name="mrp_area_id" />
<field name="name" />
<field name="origin" />
<field name="fixed" />
</group>
<group>
<field name="mrp_action" />
<field name="order_release_date" />
<field name="due_date" />
<field name="mrp_qty" />
<field name="qty_released" />
</group>
</group>
<group groups="base.group_no_one">
<field name="mrp_move_down_ids" readonly="True" />
</group>
</sheet>
</form>
</field>
</record>
<record id="mrp_planned_order_view_pivot" model="ir.ui.view">
<field name="name">mrp.planned.order.pivot</field>
<field name="model">mrp.planned.order</field>
<field name="arch" type="xml">
<pivot>
<field name="mrp_area_id" type="row" />
<field name="product_id" type="row" />
<field name="mrp_qty" type="measure" />
<field name="due_date" interval="week" type="col" />
</pivot>
</field>
</record>
<record id="mrp_planned_order_view_search" model="ir.ui.view">
<field name="name">mrp.planned.order.search</field>
<field name="model">mrp.planned.order</field>
<field name="arch" type="xml">
<search>
<field name="origin" />
<field name="name" />
<field name="product_id" />
<field name="mrp_area_id" />
<separator />
<filter
string="My products"
name="mrp_planner_id"
domain="[('mrp_planner_id', '=', uid)]"
/>
<filter string="Fixed" name="fixed" domain="[('fixed','=',True)]" />
<group name='group_by' expand="0" string="Group By...">
<filter
name='product_parameters'
string="Product Parameters"
context="{'group_by':'product_mrp_area_id'}"
/>
<filter
name='due_date'
string="Due Date"
context="{'group_by':'due_date'}"
/>
</group>
</search>
</field>
</record>
<record id="mrp_planned_order_action" model="ir.actions.act_window">
<field name="name">Planned Orders</field>
<field name="res_model">mrp.planned.order</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">list,form,pivot</field>
<field name="context">{'search_default_fixed': 1}</field>
</record>
<menuitem
name="Planned Orders"
id="menu_mrp_planned_order"
action="mrp_planned_order_action"
parent="mrp.mrp_planning_menu_root"
sequence="20"
/>
<record id="action_server_planned_order_toggle_fixed" model="ir.actions.server">
<field name="name">Toggle Fixed</field>
<field name="model_id" ref="mrp_multi_level.model_mrp_planned_order" />
<field name="binding_model_id" ref="mrp_multi_level.model_mrp_planned_order" />
<field name="state">code</field>
<field name="code">records.action_toggle_fixed()</field>
</record>
<record id="act_mrp_inventory_procure_planned_order" 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_planned_order" />
</record>
</odoo>

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="product_mrp_area_list">
<field name="name">product.mrp.area.list</field>
<field name="model">product.mrp.area</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list>
<field name="mrp_area_id" />
<field name="company_id" groups="base.group_multi_company" />
<field name="product_tmpl_id" />
<field name="product_id" groups="product.group_product_variant" />
<field name="mrp_exclude" />
<field name="mrp_verified" />
<field name="mrp_nbr_days" />
<field name="mrp_transit_delay" />
<field name="mrp_inspection_delay" />
<field name="mrp_minimum_stock" />
<field name="mrp_minimum_order_qty" />
<field name="mrp_maximum_order_qty" />
<field name="mrp_qty_multiple" />
<field name="supply_method" />
<field name="main_supplierinfo_id" />
</list>
</field>
</record>
<record model="ir.ui.view" id="product_mrp_area_form">
<field name="name">product.mrp.area.form</field>
<field name="model">product.mrp.area</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Product MRP Area parameters">
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_view_incoming_stock_moves"
string="Incoming Moves"
type="object"
class="oe_stat_button"
icon="fa-list"
/>
<button
name="action_view_outgoing_stock_moves"
string="Outgoing Moves"
type="object"
class="oe_stat_button"
icon="fa-list"
/>
</div>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
invisible="active"
/>
<group colspan="4" col="2">
<group>
<field name="active" invisible="1" />
<field name="mrp_area_id" />
<field
name="company_id"
groups="base.group_multi_company"
/>
<field name="product_tmpl_id" invisible="1" />
<field name="product_id" />
<field name="mrp_planner_id" />
<field name="location_id" invisible="1" />
<field
name="location_proc_id"
options="{'no_create': True}"
/>
</group>
<group>
<field name="mrp_exclude" />
<field name="mrp_verified" />
<field name="mrp_nbr_days" />
<!--hide delays for now-->
<field name="mrp_transit_delay" invisible="1" />
<field name="mrp_inspection_delay" invisible="1" />
<field name="mrp_minimum_stock" />
<label for="mrp_minimum_order_qty" />
<div name="mrp_minimum_order_qty" class="o_row">
<field name="mrp_minimum_order_qty" />
<span name="update_min_qty">
<button
string="Get from main supplier"
type="object"
name="update_min_qty_from_main_supplier"
invisible="supply_method != 'buy' and not main_supplierinfo_id"
class="oe_link pt-0 oe_inline"
/>
</span>
</div>
<field name="mrp_maximum_order_qty" />
<field name="mrp_qty_multiple" />
<field name="supply_method" />
<field
name="distribution_lead_time"
invisible="supply_method not in ('pull', 'push', 'pull_push')"
/>
<field name="mrp_lead_time" />
<field
name="main_supplierinfo_id"
invisible="supply_method != 'buy'"
/>
</group>
</group>
<notebook>
<page
name="mrp_moves"
string="MRP Moves"
groups="base.group_no_one"
>
<field name="mrp_move_ids" nolabel="1">
<list>
<field name="mrp_date" />
<field name="current_date" />
<field name="mrp_origin" />
<field name="state" />
<field name="mrp_order_number" />
<field name="parent_product_id" />
<field name="name" />
<field name="mrp_qty" />
<field name="current_qty" />
<field name="mrp_type" />
<field name="planned_order_up_ids" />
</list>
</field>
</page>
<page
name="planned_orders"
string="Planned Orders"
groups="base.group_no_one"
>
<field name="planned_order_ids" nolabel="1">
<list>
<field name="order_release_date" />
<field name="due_date" />
<field name="name" />
<field name="mrp_qty" />
<field name="mrp_action" />
<field name="qty_released" />
<field name="fixed" />
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="product_mrp_area_search">
<field name="name">product.mrp.area.search</field>
<field name="model">product.mrp.area</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Product MRP Area parameters">
<field name="product_id" />
<field name="mrp_area_id" />
<separator />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
<separator />
<filter
string="My products"
name="mrp_planner_id"
domain="[('mrp_planner_id', '=', uid)]"
/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="product_mrp_area_action">
<field name="name">Product MRP Area Parameters</field>
<field name="res_model">product.mrp.area</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="product_mrp_area_list" />
<field name="search_view_id" ref="product_mrp_area_search" />
</record>
</odoo>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="view_mrp_product_product_form">
<field name="name">view.product.mrp.area.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">
<div name="button_box" position="inside">
<button
type="object"
name="action_view_mrp_area_parameters"
class="oe_stat_button"
icon="fa-eject"
groups="mrp.group_mrp_user"
>
<field name="mrp_area_count" widget="statinfo" string="MRP Areas" />
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="product_template_only_form_view_mrp" model="ir.ui.view">
<field name="name">product.template.product.form.mrp</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
type="object"
name="action_view_mrp_area_parameters"
class="oe_stat_button"
icon="fa-eject"
groups="mrp.group_mrp_user"
>
<field name="mrp_area_count" widget="statinfo" string="MRP Areas" />
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<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">
<xpath expr="//button" position="before">
<button
type="object"
name="action_view_mrp_area_location"
class="oe_stat_button"
icon="fa-eject"
>
<field name="mrp_area_count" widget="statinfo" string="MRP Areas" />
</button>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,151 @@
# Copyright 2018-21 ForgeFlow S.L. (https://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.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, 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,
"supply_method": planned_order.product_mrp_area_id.supply_method,
}
@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().fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
)
@api.model
def default_get(self, fields):
res = super().default_get(fields)
active_ids = self.env.context["active_ids"] or []
active_model = self.env.context["active_model"]
if not active_ids or "item_ids" not in fields:
return res
items = item_obj = self.env["mrp.inventory.procure.item"]
if active_model == "mrp.inventory":
mrp_inventory_obj = self.env[active_model]
for line in mrp_inventory_obj.browse(active_ids).mapped(
"planned_order_ids"
):
if line.qty_released < line.mrp_qty:
items += item_obj.create(self._prepare_item(line))
elif active_model == "mrp.planned.order":
mrp_planned_order_obj = self.env[active_model]
for line in mrp_planned_order_obj.browse(active_ids):
if line.mrp_action == "phantom":
continue
if line.qty_released < line.mrp_qty:
items += item_obj.create(self._prepare_item(line))
if items:
res["item_ids"] = [(6, 0, items.ids)]
return res
def make_procurement(self):
self.ensure_one()
errors = []
pg = self.env["procurement.group"]
procurements = []
for item in self.item_ids:
if not item.qty:
raise ValidationError(_("Quantity must be positive."))
values = item._prepare_procurement_values()
procurements.append(
pg.Procurement(
item.product_id,
item.qty,
item.uom_id,
item.location_id,
"MRP: " + (item.planned_order_id.name or self.env.user.login),
"MRP: " + (item.planned_order_id.origin or self.env.user.login),
item.mrp_inventory_id.company_id,
values,
)
)
# Run procurements
try:
pg.run(procurements)
for item in self.item_ids:
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"}
class MrpInventoryProcureItem(models.TransientModel):
_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,
)
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=True)
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")
supply_method = fields.Selection(
selection=[
("buy", "Buy"),
("none", "Undefined"),
("manufacture", "Produce"),
("pull", "Pull From"),
("push", "Push To"),
("pull_push", "Pull & Push"),
],
readonly=True,
)
def _prepare_procurement_values(self, group=False):
return {
"date_planned": self.date_planned,
"warehouse_id": self.warehouse_id,
"group_id": group,
"planned_order_id": self.planned_order_id.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
)

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8" ?>
<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">
<list nocreate="1" editable="top">
<field name="mrp_inventory_id" column_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="uom.group_uom" />
<field name="date_planned" />
<field name="supply_method" />
</list>
</field>
<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,924 @@
# Copyright 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
# Copyright 2016-19 ForgeFlow S.L. (https://www.forgeflow.com)
# - Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
# - Lois Rilo <lois.rilo@forgeflow.com>
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
import logging
from datetime import date, timedelta
from odoo import _, api, exceptions, fields, models
from odoo.tools import float_is_zero, mute_logger
logger = logging.getLogger(__name__)
class MultiLevelMrp(models.TransientModel):
_name = "mrp.multi.level"
_description = "Multi Level MRP"
mrp_area_ids = fields.Many2many(
comodel_name="mrp.area",
string="MRP Areas to run",
help="If empty, all areas will be computed.",
)
@api.model
def _prepare_mrp_move_data_from_stock_move(
self, product_mrp_area, move, direction="in"
):
area = product_mrp_area.mrp_area_id
if direction == "out":
mrp_type = "d"
product_qty = -move.product_qty
else:
mrp_type = "s"
product_qty = move.product_qty
po = po_line = None
mo = origin = order_number = order_origin = parent_product_id = None
if move.purchase_line_id:
po = move.purchase_line_id.order_id
order_number = po.name
order_origin = po.origin
origin = "po"
po = move.purchase_line_id.order_id.id
po_line = move.purchase_line_id.id
elif move.production_id or move.raw_material_production_id:
production = move.production_id or move.raw_material_production_id
order_number = production.name
order_origin = production.origin
origin = "mo"
mo = production.id
elif move.move_dest_ids:
for move_dest_id in move.move_dest_ids.filtered("production_id"):
production = move_dest_id.production_id
order_number = production.name
order_origin = production.origin
origin = "mo"
mo = move_dest_id.production_id.id
parent_product_id = (
move_dest_id.production_id.product_id or move_dest_id.product_id
).id
if not order_number:
source = (move.picking_id or move).origin
order_number = source or (move.picking_id or move).name
origin = "mv"
# The date to display is based on the timezone of the warehouse.
today_tz = area._datetime_to_date_tz()
move_date_tz = area._datetime_to_date_tz(move.date)
if move_date_tz > today_tz:
mrp_date = move_date_tz
else:
mrp_date = today_tz
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,
"mrp_type": mrp_type,
"mrp_origin": origin or "",
"mrp_order_number": order_number,
"parent_product_id": parent_product_id,
"name": order_number,
"origin": order_origin,
"state": move.state,
}
@api.model
def _prepare_planned_order_data(
self, product_mrp_area, qty, mrp_date_supply, mrp_action_date, name, values
):
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": "Planned supply for: " + name,
"origin": values.get("origin") or name,
"fixed": False,
}
@api.model
def _prepare_mrp_move_data_bom_explosion(
self,
product,
bomline,
qty,
mrp_date_demand_2,
bom,
name,
planned_order,
values=None,
):
product_mrp_area = self._get_product_mrp_area_from_product_and_area(
bomline.product_id, product.mrp_area_id
)
if not product_mrp_area:
raise exceptions.Warning(_("No MRP product found"))
factor = (
product.product_id.uom_id._compute_quantity(
qty, bomline.bom_id.product_uom_id
)
/ bomline.bom_id.product_qty
)
line_quantity = factor * bomline.product_qty
return {
"mrp_area_id": product_mrp_area.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": -line_quantity, # 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: %s"
% (name or product.product_id.default_code or product.product_id.name)
).replace(
"Demand Bom Explosion: Demand Bom Explosion: ", "Demand Bom Explosion: "
),
"origin": planned_order.origin if planned_order else values.get("origin"),
"bom_id": bom.id,
}
@api.model
def _get_action_and_supply_dates(self, product_mrp_area, mrp_date):
if not isinstance(mrp_date, date):
mrp_date = fields.Date.from_string(mrp_date)
if mrp_date < date.today():
mrp_date_supply = date.today()
else:
mrp_date_supply = mrp_date
calendar = product_mrp_area.mrp_area_id.calendar_id
if calendar and product_mrp_area.mrp_lead_time:
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)
mrp_action_date = res.date()
else:
mrp_action_date = mrp_date - timedelta(days=product_mrp_area.mrp_lead_time)
return mrp_action_date, mrp_date_supply
@api.model
def _get_bom_to_explode(self, product_mrp_area_id):
return product_mrp_area_id.supply_bom_id
@api.model
def explode_action(
self, product_mrp_area_id, mrp_action_date, name, qty, action, values=None
):
"""Explode requirements."""
mrp_date_demand = mrp_action_date
if mrp_date_demand < date.today():
mrp_date_demand = date.today()
bom = self._get_bom_to_explode(product_mrp_area_id)
if not bom:
return False
pd = self.env["decimal.precision"].precision_get("Product Unit of Measure")
for bomline in bom.bom_line_ids:
if (
float_is_zero(bomline.product_qty, precision_digits=pd)
or bomline.product_id.type != "consu"
):
continue
if self.with_context(mrp_explosion=True)._exclude_from_mrp(
bomline.product_id, product_mrp_area_id.mrp_area_id
):
# Stop explosion.
continue
if bomline._skip_bom_line(product_mrp_area_id.product_id):
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,
action,
values,
)
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):
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
)
return self.create_planned_order(
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 = 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
return values
qty_ordered = values.get("qty_ordered", 0.0) if values else 0.0
qty_to_order = mrp_qty
while qty_ordered < mrp_qty:
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, values
)
planned_order = False
if product_mrp_area_id._should_create_planned_order():
planned_order = self.env["mrp.planned.order"].create(order_data)
qty_ordered = qty_ordered + qty
if product_mrp_area_id._to_be_exploded():
self.explode_action(
product_mrp_area_id,
mrp_action_date,
name,
qty,
planned_order,
values,
)
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,
qty_ordered,
)
logger.debug(log_msg)
return values
@api.model
def _mrp_cleanup(self, mrp_areas):
logger.info("Start MRP Cleanup")
domain = []
if mrp_areas:
domain += [("mrp_area_id", "in", mrp_areas.ids)]
with mute_logger("odoo.models.unlink"):
self.env["mrp.move"].search(domain).unlink()
self.env["mrp.planned.order"].search(
domain + [("fixed", "=", False)]
).unlink()
self.env["mrp.inventory"].search(domain).unlink()
logger.info("End MRP Cleanup")
return True
def _domain_bom_lines_by_llc(self, llc, product_templates):
return [
("product_id.llc", "=", llc),
("bom_id.product_tmpl_id", "in", product_templates.ids),
("bom_id.active", "=", True),
]
def _get_bom_lines_by_llc(self, llc, product_templates):
return self.env["mrp.bom.line"].search(
self._domain_bom_lines_by_llc(llc, product_templates)
)
@api.model
def _low_level_code_calculation(self):
logger.info("Start low level code calculation")
counter = 999999
llc = 0
llc_recursion_limit = (
int(
self.env["ir.config_parameter"]
.sudo()
.get_param("mrp_multi_level.llc_calculation_recursion_limit")
)
or 1000
)
self.env["product.product"].search([]).write({"llc": llc})
products = self.env["product.product"].search([("llc", "=", llc)])
if products:
counter = len(products)
log_msg = f"Low level code 0 finished - Nbr. products: {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._get_bom_lines_by_llc(llc - 1, p_templates)
products = bom_lines.mapped("product_id")
products.write({"llc": llc})
counter = self.env["product.product"].search_count([("llc", "=", llc)])
log_msg = f"Low level code {llc} finished - Nbr. products: {counter}"
logger.info(log_msg)
if llc > llc_recursion_limit:
logger.error("Recursion limit reached during LLC calculation.")
break
mrp_lowest_llc = llc
logger.info("End low level code calculation")
return mrp_lowest_llc
@api.model
def _adjust_mrp_applicable(self, mrp_areas):
"""This method is meant to modify the products that are applicable
to MRP Multi level calculation
"""
return True
@api.model
def _calculate_mrp_applicable(self, mrp_areas):
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", "=", "consu"),
("product_id.is_storable", "=", True),
]
self.env["product.mrp.area"].search(domain).write({"mrp_applicable": True})
self._adjust_mrp_applicable(mrp_areas)
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(count_domain)
log_msg = f"End Calculate MRP Applicable: {counter}"
logger.info(log_msg)
return True
@api.model
def _init_mrp_move_from_forecast(self, product_mrp_area):
"""This method is meant to be inherited to add a forecast mechanism."""
return True
@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"]
in_domain = product_mrp_area._in_stock_moves_domain()
in_moves = move_obj.search(in_domain)
out_domain = product_mrp_area._out_stock_moves_domain()
out_moves = move_obj.search(out_domain)
move_vals = []
if in_moves:
for move in in_moves:
move_data = self._prepare_mrp_move_data_from_stock_move(
product_mrp_area, move, direction="in"
)
if move_data:
move_vals.append(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"
)
if move_data:
move_vals.append(move_data)
mrp_move_obj.create(move_vals)
return True
@api.model
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_uom_qty,
"current_qty": poline.product_uom_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._get_locations()
picking_types = self.env["stock.picking.type"].search(
[("default_location_dest_id", "child_of", 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),
]
)
mrp_move_vals = []
for line in po_lines:
mrp_move_data = self._prepare_mrp_move_data_from_purchase_order(
line, product_mrp_area
)
mrp_move_vals.append(mrp_move_data)
if mrp_move_vals:
self.env["mrp.move"].create(mrp_move_vals)
@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,
)
@api.model
def _init_mrp_move(self, product_mrp_area):
self._init_mrp_move_from_forecast(product_mrp_area)
self._init_mrp_move_from_stock_move(product_mrp_area)
self._init_mrp_move_from_purchase_order(product_mrp_area)
@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,
)
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")
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)]
)
init_counter = 0
for mrp_area in mrp_areas:
for product_mrp_area in product_mrp_areas.filtered(
lambda a, mrp_area=mrp_area: a.mrp_area_id == mrp_area
):
if self._exclude_from_mrp(product_mrp_area.product_id, mrp_area):
continue
init_counter += 1
log_msg = f"MRP Init: {init_counter} - {product_mrp_area.display_name} "
logger.info(log_msg)
self._init_mrp_move(product_mrp_area)
logger.info("End MRP initialisation")
def _get_qty_to_order(self, product_mrp_area, date, move_qty, onhand):
"""Compute the qty to order at a given date, for a product MRP area, given an
mrp.move quantity and an onhand quantity.
This method is an extension point, allowing a new module to change the way this
quantity should be computed.
"""
# The default rule is to resupply to rebuild the safety stock
return product_mrp_area.mrp_minimum_stock - onhand - move_qty
@api.model
def _init_mrp_move_grouped_demand(self, product_mrp_area):
last_date = None
last_qty = 0.00
onhand = (
0.0
if product_mrp_area.supply_method == "phantom"
else product_mrp_area.qty_available
)
grouping_delta = product_mrp_area.mrp_nbr_days
demand_origin = []
if (
product_mrp_area.mrp_move_ids
and onhand < product_mrp_area.mrp_minimum_stock
):
last_date = self._get_safety_stock_target_date(product_mrp_area)
demand_origin.append("Safety Stock")
move = fields.first(product_mrp_area.mrp_move_ids)
if last_date and (
fields.Date.from_string(move.mrp_date)
>= last_date + timedelta(days=grouping_delta)
):
name = _("Safety Stock")
origin = ",".join(list({x for x in demand_origin if x}))
qtytoorder = self._get_qty_to_order(
product_mrp_area, last_date, 0, onhand
)
cm = self.create_action(
product_mrp_area_id=product_mrp_area,
mrp_date=last_date,
mrp_qty=qtytoorder,
name=name,
values=dict(origin=origin),
)
qty_ordered = cm.get("qty_ordered", 0.0)
onhand = onhand + qty_ordered
last_date = None
last_qty = 0.00
demand_origin = []
for move in product_mrp_area.mrp_move_ids:
if self._exclude_move(move):
continue
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 of %(product_name)s for %(delta_days)d Days"
) % dict(
product_name=product_mrp_area.product_id.display_name,
delta_days=grouping_delta,
)
origin = ",".join(list({x for x in demand_origin if x}))
qtytoorder = self._get_qty_to_order(
product_mrp_area, last_date, last_qty, onhand
)
cm = self.create_action(
product_mrp_area_id=product_mrp_area,
mrp_date=last_date,
mrp_qty=qtytoorder,
name=name,
values=dict(origin=origin),
)
qty_ordered = cm.get("qty_ordered", 0.0)
onhand = onhand + last_qty + qty_ordered
last_date = None
last_qty = 0.00
demand_origin = []
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:
last_date = fields.Date.from_string(move.mrp_date)
last_qty = move.mrp_qty
else:
last_qty += move.mrp_qty
else:
last_date = fields.Date.from_string(move.mrp_date)
onhand += move.mrp_qty
if move.mrp_type == "d":
demand_origin.append(move.origin or move.name)
if last_date and last_qty != 0.00:
name = _(
"Grouped Demand of %(product_name)s for %(delta_days)d Days"
) % dict(
product_name=product_mrp_area.product_id.display_name,
delta_days=grouping_delta,
)
origin = ",".join(list({x for x in demand_origin if x}))
qtytoorder = self._get_qty_to_order(
product_mrp_area, last_date, last_qty, onhand
)
cm = self.create_action(
product_mrp_area_id=product_mrp_area,
mrp_date=last_date,
mrp_qty=qtytoorder,
name=name,
values=dict(origin=origin),
)
qty_ordered = cm.get("qty_ordered", 0.0)
onhand += qty_ordered
last_qty -= qty_ordered
if (onhand + last_qty) < product_mrp_area.mrp_minimum_stock:
mrp_date = self._get_safety_stock_target_date(product_mrp_area)
qtytoorder = self._get_qty_to_order(product_mrp_area, mrp_date, 0, onhand)
name = _("Safety Stock")
cm = self.create_action(
product_mrp_area_id=product_mrp_area,
mrp_date=mrp_date,
mrp_qty=qtytoorder,
name=name,
values=dict(origin=name),
)
qty_ordered = cm["qty_ordered"]
onhand += qty_ordered
def _get_safety_stock_target_date(self, product_mrp_area):
"""Get the date at which the safety stock rebuild should be targeted
This method is an extension point for modules who need to cusomize that date."""
return date.today()
@api.model
def _init_mrp_move_non_grouped_demand(self, product_mrp_area):
onhand = (
0.0
if product_mrp_area.supply_method == "phantom"
else product_mrp_area.qty_available
)
for move in product_mrp_area.mrp_move_ids:
if self._exclude_move(move):
continue
# This works because mrp moves are ordered by:
# product_mrp_area_id, mrp_date, mrp_type desc, id
if onhand + move.mrp_qty < product_mrp_area.mrp_minimum_stock:
qtytoorder = self._get_qty_to_order(
product_mrp_area,
self._get_safety_stock_target_date(product_mrp_area),
0,
onhand,
)
name = _("Safety Stock")
cm = self.create_action(
product_mrp_area_id=product_mrp_area,
mrp_date=self._get_safety_stock_target_date(product_mrp_area),
mrp_qty=qtytoorder,
name=name,
values=dict(origin=name),
)
qty_ordered = cm["qty_ordered"]
onhand += qty_ordered
qtytoorder = self._get_qty_to_order(
product_mrp_area, move.mrp_date, move.mrp_qty, onhand
)
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 or "",
values=dict(origin=move.origin or ""),
)
qty_ordered = cm["qty_ordered"]
onhand += move.mrp_qty + qty_ordered
else:
onhand += move.mrp_qty
if onhand < product_mrp_area.mrp_minimum_stock:
mrp_date = self._get_safety_stock_target_date(product_mrp_area)
qtytoorder = self._get_qty_to_order(product_mrp_area, mrp_date, 0, onhand)
name = _("Safety Stock")
cm = self.create_action(
product_mrp_area_id=product_mrp_area,
mrp_date=mrp_date,
mrp_qty=qtytoorder,
name=name,
values=dict(origin=name),
)
qty_ordered = cm["qty_ordered"]
onhand += qty_ordered
@api.model
def _exclude_move(self, move):
"""Improve extensibility being able to exclude special moves."""
return False
def _get_mrp_initialization_groups_of_params(self, mrp_lowest_llc, mrp_areas):
product_mrp_area_obj = self.env["product.mrp.area"]
groups = {}
for mrp_area in mrp_areas:
llc = 0
while mrp_lowest_llc > llc:
groups[mrp_area, llc] = product_mrp_area_obj.search(
[("product_id.llc", "=", llc), ("mrp_area_id", "=", mrp_area.id)]
)
llc += 1
return groups
@api.model
def _mrp_calculation(self, mrp_lowest_llc, mrp_areas):
logger.info("Start MRP calculation")
if not mrp_areas:
mrp_areas = self.env["mrp.area"].search([])
keyed_groups = self._get_mrp_initialization_groups_of_params(
mrp_lowest_llc, mrp_areas
)
for (mrp_area, llc), product_mrp_areas in keyed_groups.items():
counter = 0
for product_mrp_area in product_mrp_areas:
if product_mrp_area.mrp_nbr_days == 0:
self._init_mrp_move_non_grouped_demand(product_mrp_area)
else:
self._init_mrp_move_grouped_demand(product_mrp_area)
counter += 1
log_msg = (
f"MRP Calculation LLC {llc} at {mrp_area.name} Finished "
"- Nbr. products: {counter}"
)
logger.info(log_msg)
logger.info("End MRP calculation")
@api.model
def _get_demand_groups(self, product_mrp_area):
query = """
SELECT mrp_date, sum(mrp_qty)
FROM mrp_move
WHERE product_mrp_area_id = %(mrp_product)s
AND mrp_type = 'd'
GROUP BY mrp_date
"""
params = {"mrp_product": product_mrp_area.id}
return query, params
@api.model
def _get_supply_groups(self, product_mrp_area):
query = """
SELECT mrp_date, sum(mrp_qty)
FROM mrp_move
WHERE product_mrp_area_id = %(mrp_product)s
AND mrp_type = 's'
GROUP BY mrp_date
"""
params = {"mrp_product": product_mrp_area.id}
return query, params
@api.model
def _get_planned_order_groups(self, product_mrp_area):
query = """
SELECT due_date, sum(mrp_qty)
FROM mrp_planned_order
WHERE product_mrp_area_id = %(mrp_product)s
GROUP BY due_date
"""
params = {"mrp_product": product_mrp_area.id}
return query, params
@api.model
def _prepare_mrp_inventory_data(
self,
product_mrp_area,
mdt,
on_hand_qty,
running_availability,
demand_qty_by_date,
supply_qty_by_date,
planned_qty_by_date,
):
"""Return dict to create mrp.inventory records on MRP Multi Level Scheduler"""
mrp_inventory_data = {"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)
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
if product_mrp_area.supply_method != "phantom":
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
return mrp_inventory_data, running_availability, on_hand_qty
@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"]
# Read Demand
demand_qty_by_date = {}
query, params = self._get_demand_groups(product_mrp_area)
self.env.cr.execute(query, params)
for mrp_date, qty in self.env.cr.fetchall():
demand_qty_by_date[mrp_date] = qty
# Read Supply
supply_qty_by_date = {}
query, params = self._get_supply_groups(product_mrp_area)
self.env.cr.execute(query, params)
for mrp_date, qty in self.env.cr.fetchall():
supply_qty_by_date[mrp_date] = qty
# Read planned orders:
planned_qty_by_date = {}
query, params = self._get_planned_order_groups(product_mrp_area)
self.env.cr.execute(query, params)
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")
mrp_dates = set(moves_dates + action_dates)
on_hand_qty = (
0.0
if product_mrp_area.supply_method == "phantom"
else product_mrp_area.qty_available
)
running_availability = on_hand_qty
mrp_inventory_vals = []
for mdt in sorted(mrp_dates):
(
mrp_inventory_data,
running_availability,
on_hand_qty,
) = self._prepare_mrp_inventory_data(
product_mrp_area,
mdt,
on_hand_qty,
running_availability,
demand_qty_by_date,
supply_qty_by_date,
planned_qty_by_date,
)
mrp_inventory_vals.append(mrp_inventory_data)
if mrp_inventory_vals:
mrp_invs = self.env["mrp.inventory"].create(mrp_inventory_vals)
planned_orders = planned_order_obj.search(
[("product_mrp_area_id", "=", product_mrp_area.id)]
)
# attach planned orders to inventory
for po in planned_orders:
invs = mrp_invs.filtered(lambda i, po=po: i.date == po.due_date)
if invs:
po.mrp_inventory_id = invs[0]
def should_build_time_phased_inventory(self, product_mrp_area):
return not (
self._exclude_from_mrp(
product_mrp_area.product_id, product_mrp_area.mrp_area_id
)
or product_mrp_area.supply_method == "phantom"
)
@api.model
def _mrp_final_process(self, mrp_areas):
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)
for product_mrp_area in product_mrp_area_ids:
# Build the time-phased inventory
if not self.should_build_time_phased_inventory(product_mrp_area):
continue
self._init_mrp_inventory(product_mrp_area)
logger.info("End MRP final process")
def run_mrp_multi_level(self):
self._mrp_cleanup(self.mrp_area_ids)
mrp_lowest_llc = self._low_level_code_calculation()
self._calculate_mrp_applicable(self.mrp_area_ids)
self._mrp_initialisation(self.mrp_area_ids)
self._mrp_calculation(mrp_lowest_llc, self.mrp_area_ids)
self._mrp_final_process(self.mrp_area_ids)
# Open MRP inventory screen to show result if manually run:
# Done as sudo to allow non-admin users to read the action.
action = self.env.ref("mrp_multi_level.mrp_inventory_action")
result = action.sudo().read()[0]
return result

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<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 MRP Multi Level">
<group>
<field
name="mrp_area_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
</group>
<footer>
<button
name="run_mrp_multi_level"
string="Run MRP"
type="object"
class="oe_highlight"
/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_mrp_multi_level" model="ir.actions.act_window">
<field name="name">Run MRP</field>
<field name="res_model">mrp.multi.level</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_mrp_multi_level" />
</record>
</odoo>