mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
271
mrp_multi_level/README.rst
Normal file
271
mrp_multi_level/README.rst
Normal 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.
|
||||
2
mrp_multi_level/__init__.py
Normal file
2
mrp_multi_level/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
45
mrp_multi_level/__manifest__.py
Normal file
45
mrp_multi_level/__manifest__.py
Normal 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,
|
||||
}
|
||||
8
mrp_multi_level/data/mrp_area_data.xml
Normal file
8
mrp_multi_level/data/mrp_area_data.xml
Normal 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>
|
||||
12
mrp_multi_level/data/mrp_multi_level_cron.xml
Normal file
12
mrp_multi_level/data/mrp_multi_level_cron.xml
Normal 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>
|
||||
7
mrp_multi_level/data/system_parameter.xml
Normal file
7
mrp_multi_level/data/system_parameter.xml
Normal 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>
|
||||
43
mrp_multi_level/demo/initial_on_hand_demo.xml
Normal file
43
mrp_multi_level/demo/initial_on_hand_demo.xml
Normal 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>
|
||||
178
mrp_multi_level/demo/mrp_bom_demo.xml
Normal file
178
mrp_multi_level/demo/mrp_bom_demo.xml
Normal 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>
|
||||
6
mrp_multi_level/demo/product_category_demo.xml
Normal file
6
mrp_multi_level/demo/product_category_demo.xml
Normal 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>
|
||||
71
mrp_multi_level/demo/product_mrp_area_demo.xml
Normal file
71
mrp_multi_level/demo/product_mrp_area_demo.xml
Normal 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>
|
||||
176
mrp_multi_level/demo/product_product_demo.xml
Normal file
176
mrp_multi_level/demo/product_product_demo.xml
Normal 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>
|
||||
61
mrp_multi_level/demo/product_supplierinfo_demo.xml
Normal file
61
mrp_multi_level/demo/product_supplierinfo_demo.xml
Normal 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>
|
||||
8
mrp_multi_level/demo/res_partner_demo.xml
Normal file
8
mrp_multi_level/demo/res_partner_demo.xml
Normal 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>
|
||||
BIN
mrp_multi_level/docs/mrp_multi_level_test_run.ods
Normal file
BIN
mrp_multi_level/docs/mrp_multi_level_test_run.ods
Normal file
Binary file not shown.
1164
mrp_multi_level/i18n/de.po
Normal file
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
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
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
1045
mrp_multi_level/i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
1024
mrp_multi_level/i18n/mrp_multi_level.pot
Normal file
1024
mrp_multi_level/i18n/mrp_multi_level.pot
Normal file
File diff suppressed because it is too large
Load Diff
11
mrp_multi_level/models/__init__.py
Normal file
11
mrp_multi_level/models/__init__.py
Normal 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
|
||||
42
mrp_multi_level/models/mrp_area.py
Normal file
42
mrp_multi_level/models/mrp_area.py
Normal 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
|
||||
135
mrp_multi_level/models/mrp_inventory.py
Normal file
135
mrp_multi_level/models/mrp_inventory.py
Normal 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,
|
||||
}
|
||||
95
mrp_multi_level/models/mrp_move.py
Normal file
95
mrp_multi_level/models/mrp_move.py
Normal 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")
|
||||
115
mrp_multi_level/models/mrp_planned_order.py
Normal file
115
mrp_multi_level/models/mrp_planned_order.py
Normal 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
|
||||
15
mrp_multi_level/models/mrp_production.py
Normal file
15
mrp_multi_level/models/mrp_production.py
Normal 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,
|
||||
)
|
||||
312
mrp_multi_level/models/product_mrp_area.py
Normal file
312
mrp_multi_level/models/product_mrp_area.py
Normal 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
|
||||
71
mrp_multi_level/models/product_product.py
Normal file
71
mrp_multi_level/models/product_product.py
Normal 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
|
||||
49
mrp_multi_level/models/product_template.py
Normal file
49
mrp_multi_level/models/product_template.py
Normal 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
|
||||
41
mrp_multi_level/models/stock_location.py
Normal file
41
mrp_multi_level/models/stock_location.py
Normal 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
|
||||
16
mrp_multi_level/models/stock_quant.py
Normal file
16
mrp_multi_level/models/stock_quant.py
Normal 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"]
|
||||
35
mrp_multi_level/models/stock_rule.py
Normal file
35
mrp_multi_level/models/stock_rule.py
Normal 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
|
||||
3
mrp_multi_level/pyproject.toml
Normal file
3
mrp_multi_level/pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
9
mrp_multi_level/readme/CONFIGURE.md
Normal file
9
mrp_multi_level/readme/CONFIGURE.md
Normal 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.
|
||||
7
mrp_multi_level/readme/CONTRIBUTORS.md
Normal file
7
mrp_multi_level/readme/CONTRIBUTORS.md
Normal 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>\>
|
||||
19
mrp_multi_level/readme/DESCRIPTION.md
Normal file
19
mrp_multi_level/readme/DESCRIPTION.md
Normal 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).
|
||||
101
mrp_multi_level/readme/HISTORY.md
Normal file
101
mrp_multi_level/readme/HISTORY.md
Normal 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.
|
||||
12
mrp_multi_level/readme/USAGE.md
Normal file
12
mrp_multi_level/readme/USAGE.md
Normal 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*.
|
||||
16
mrp_multi_level/security/ir.model.access.csv
Normal file
16
mrp_multi_level/security/ir.model.access.csv
Normal 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
|
||||
|
57
mrp_multi_level/security/mrp_multi_level_security.xml
Normal file
57
mrp_multi_level/security/mrp_multi_level_security.xml
Normal 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>
|
||||
BIN
mrp_multi_level/static/description/icon.png
Normal file
BIN
mrp_multi_level/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
631
mrp_multi_level/static/description/index.html
Normal file
631
mrp_multi_level/static/description/index.html
Normal 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&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 PO’s, or MO’s.</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 > Configuration > 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 > Products > 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 > Planning > 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 > Planning > MRP Inventory</em>.</li>
|
||||
<li>Filter with <em>To procure</em>.</li>
|
||||
<li>Select multiple records and click on <em>Action > 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 area’s
|
||||
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 doesn’t 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 <<a class="reference external" href="mailto:wim.audenaert@ucamco.com">wim.audenaert@ucamco.com</a>></li>
|
||||
<li>Jordi Ballester <<a class="reference external" href="mailto:jordi.ballester@forgeflow.com">jordi.ballester@forgeflow.com</a>></li>
|
||||
<li>Lois Rilo <<a class="reference external" href="mailto:lois.rilo@forgeflow.com">lois.rilo@forgeflow.com</a>></li>
|
||||
<li>Héctor Villarreal <<a class="reference external" href="mailto:hector.villarreal@forgeflow.com">hector.villarreal@forgeflow.com</a>></li>
|
||||
<li>Christopher Ormaza <<a class="reference external" href="mailto:chris.ormaza@forgeflow.com">chris.ormaza@forgeflow.com</a>></li>
|
||||
<li>Joan Sisquella <<a class="reference external" href="mailto:joan.sisquella@forgeflow.com">joan.sisquella@forgeflow.com</a>></li>
|
||||
<li>Alexandre Fayolle <<a class="reference external" href="mailto:alexandre.fayolle@camptocamp.com">alexandre.fayolle@camptocamp.com</a>></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>
|
||||
BIN
mrp_multi_level/static/src/img/icon.png
Normal file
BIN
mrp_multi_level/static/src/img/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
1
mrp_multi_level/tests/__init__.py
Normal file
1
mrp_multi_level/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_mrp_multi_level
|
||||
656
mrp_multi_level/tests/common.py
Normal file
656
mrp_multi_level/tests/common.py
Normal 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
|
||||
930
mrp_multi_level/tests/test_mrp_multi_level.py
Normal file
930
mrp_multi_level/tests/test_mrp_multi_level.py
Normal 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)
|
||||
60
mrp_multi_level/views/mrp_area_views.xml
Normal file
60
mrp_multi_level/views/mrp_area_views.xml
Normal 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>
|
||||
167
mrp_multi_level/views/mrp_inventory_views.xml
Normal file
167
mrp_multi_level/views/mrp_inventory_views.xml
Normal 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 <= 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>
|
||||
33
mrp_multi_level/views/mrp_menuitem.xml
Normal file
33
mrp_multi_level/views/mrp_menuitem.xml
Normal 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>
|
||||
92
mrp_multi_level/views/mrp_move_views.xml
Normal file
92
mrp_multi_level/views/mrp_move_views.xml
Normal 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>
|
||||
139
mrp_multi_level/views/mrp_planned_order_views.xml
Normal file
139
mrp_multi_level/views/mrp_planned_order_views.xml
Normal 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>
|
||||
183
mrp_multi_level/views/product_mrp_area_views.xml
Normal file
183
mrp_multi_level/views/product_mrp_area_views.xml
Normal 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>
|
||||
22
mrp_multi_level/views/product_product_views.xml
Normal file
22
mrp_multi_level/views/product_product_views.xml
Normal 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>
|
||||
21
mrp_multi_level/views/product_template_views.xml
Normal file
21
mrp_multi_level/views/product_template_views.xml
Normal 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>
|
||||
20
mrp_multi_level/views/stock_location_views.xml
Normal file
20
mrp_multi_level/views/stock_location_views.xml
Normal 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>
|
||||
2
mrp_multi_level/wizards/__init__.py
Normal file
2
mrp_multi_level/wizards/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import mrp_multi_level
|
||||
from . import mrp_inventory_procure
|
||||
151
mrp_multi_level/wizards/mrp_inventory_procure.py
Normal file
151
mrp_multi_level/wizards/mrp_inventory_procure.py
Normal 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
|
||||
)
|
||||
67
mrp_multi_level/wizards/mrp_inventory_procure_views.xml
Normal file
67
mrp_multi_level/wizards/mrp_inventory_procure_views.xml
Normal 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>
|
||||
924
mrp_multi_level/wizards/mrp_multi_level.py
Normal file
924
mrp_multi_level/wizards/mrp_multi_level.py
Normal 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
|
||||
35
mrp_multi_level/wizards/mrp_multi_level_views.xml
Normal file
35
mrp_multi_level/wizards/mrp_multi_level_views.xml
Normal 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>
|
||||
Reference in New Issue
Block a user