mirror of
https://github.com/bringout/oca-mrp.git
synced 2026-04-23 20:12:01 +02:00
Initial commit: OCA Mrp packages (117 packages)
This commit is contained in:
commit
277e84fd7a
4403 changed files with 395154 additions and 0 deletions
46
odoo-bringout-oca-manufacture-mrp_multi_level/README.md
Normal file
46
odoo-bringout-oca-manufacture-mrp_multi_level/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# MRP Multi Level
|
||||
|
||||
Odoo addon: mrp_multi_level
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-manufacture-mrp_multi_level
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- mrp
|
||||
- purchase_stock
|
||||
- mrp_warehouse_calendar
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: MRP Multi Level
|
||||
- **Version**: 16.0.1.5.7
|
||||
- **Category**: Manufacturing
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/manufacture](https://github.com/OCA/manufacture) branch 16.0, addon `mrp_multi_level`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Mrp_multi_level Module - mrp_multi_level
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for mrp_multi_level. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [mrp](../../odoo-bringout-oca-ocb-mrp)
|
||||
- [purchase_stock](../../odoo-bringout-oca-ocb-purchase_stock)
|
||||
- [mrp_warehouse_calendar](../../odoo-bringout-oca-manufacture-mrp_warehouse_calendar)
|
||||
4
odoo-bringout-oca-manufacture-mrp_multi_level/doc/FAQ.md
Normal file
4
odoo-bringout-oca-manufacture-mrp_multi_level/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon mrp_multi_level or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-manufacture-mrp_multi_level"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-manufacture-mrp_multi_level"
|
||||
```
|
||||
22
odoo-bringout-oca-manufacture-mrp_multi_level/doc/MODELS.md
Normal file
22
odoo-bringout-oca-manufacture-mrp_multi_level/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in mrp_multi_level.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class mrp_area
|
||||
class mrp_inventory
|
||||
class mrp_move
|
||||
class mrp_planned_order
|
||||
class product_mrp_area
|
||||
class mrp_production
|
||||
class product_product
|
||||
class product_template
|
||||
class stock_location
|
||||
class stock_quant
|
||||
class stock_rule
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: mrp_multi_level. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon mrp_multi_level
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in mrp_multi_level.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../mrp_multi_level/security/ir.model.access.csv)**
|
||||
- 15 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[mrp_multi_level_security.xml](../mrp_multi_level/security/mrp_multi_level_security.xml)**
|
||||
- 2 security groups defined
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../mrp_multi_level/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[mrp_multi_level_security.xml](../mrp_multi_level/security/mrp_multi_level_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon mrp_multi_level
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
===============
|
||||
MRP Multi Level
|
||||
===============
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:72f5c1e6f5fe8ddfe32ac230c0b2a6a6f9009a523c3e415ac2b05c21f9de08af
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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/16.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-16-0/manufacture-16-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=16.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 > Master Data > Product MRP Area Parameters* and set
|
||||
the MRP parameters for a given product and area.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To manually run the MRP scheduler:
|
||||
|
||||
#. Go to *Manufacturing > Operations > Run MRP Multi Level*.
|
||||
#. On the wizard click *Run MRP*.
|
||||
|
||||
To launch replenishment orders (moves, purchases, production orders...):
|
||||
|
||||
#. Go to *Manufacturing > Operations > MRP Inventory*.
|
||||
#. Filter with *To procure*.
|
||||
#. Select multiple records and click on *Action > Procure* or click the right
|
||||
hand side gears in any record.
|
||||
#. 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:%2016.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
|
||||
.. |maintainer-ChrisOForgeFlow| image:: https://github.com/ChrisOForgeFlow.png?size=40px
|
||||
:target: https://github.com/ChrisOForgeFlow
|
||||
:alt: ChrisOForgeFlow
|
||||
|
||||
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-JordiBForgeFlow| |maintainer-LoisRForgeFlow| |maintainer-ChrisOForgeFlow|
|
||||
|
||||
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/16.0/mrp_multi_level>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -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": "16.0.1.5.7",
|
||||
"development_status": "Production/Stable",
|
||||
"license": "LGPL-3",
|
||||
"author": "Ucamco, ForgeFlow, Odoo Community Association (OCA)",
|
||||
"maintainers": ["JordiBForgeFlow", "LoisRForgeFlow", "ChrisOForgeFlow"],
|
||||
"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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="mrp_multi_level_cron" model="ir.cron">
|
||||
<field name="name">Multi Level MRP</field>
|
||||
<field name="model_id" ref="mrp_multi_level.model_mrp_multi_level" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.run_mrp_multi_level()</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<?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>
|
||||
</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>
|
||||
</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>
|
||||
</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_attribute_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_attribute_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_attribute_value_3')])]"
|
||||
/>
|
||||
</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_attribute_value_4')])]"
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="product_product_fp_1" model="product.product">
|
||||
<field name="name">FP-1</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">2</field>
|
||||
<field
|
||||
name="route_ids"
|
||||
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
|
||||
/>
|
||||
</record>
|
||||
<record id="product_product_fp_2" model="product.product">
|
||||
<field name="name">FP-2</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">1</field>
|
||||
<field
|
||||
name="route_ids"
|
||||
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
|
||||
/>
|
||||
</record>
|
||||
<record id="product_product_fp_3" model="product.product">
|
||||
<field name="name">FP-3</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">3</field>
|
||||
<field
|
||||
name="route_ids"
|
||||
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
|
||||
/>
|
||||
</record>
|
||||
<record id="product_product_fp_4" model="product.product">
|
||||
<field name="name">FP-4</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">2</field>
|
||||
</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="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">1</field>
|
||||
<field
|
||||
name="route_ids"
|
||||
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
|
||||
/>
|
||||
</record>
|
||||
<record id="product_product_sf_2" model="product.product">
|
||||
<field name="name">SF-2</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">3</field>
|
||||
<field
|
||||
name="route_ids"
|
||||
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
|
||||
/>
|
||||
</record>
|
||||
<record id="product_product_sf_3" model="product.product">
|
||||
<field name="name">SF-3</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="uom.product_uom_unit" />
|
||||
<field name="uom_po_id" ref="uom.product_uom_unit" />
|
||||
<field name="produce_delay">3</field>
|
||||
<field
|
||||
name="route_ids"
|
||||
eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"
|
||||
/>
|
||||
</record>
|
||||
|
||||
<record id="product_product_pp_1" model="product.product">
|
||||
<field name="name">PP-1</field>
|
||||
<field name="categ_id" ref="product_category_mrp" />
|
||||
<field name="type">product</field>
|
||||
<field name="uom_id" ref="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="type">product</field>
|
||||
<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="type">product</field>
|
||||
<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="type">product</field>
|
||||
<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="type">product</field>
|
||||
<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="type">product</field>
|
||||
<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="type">product</field>
|
||||
<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="type">product</field>
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="res_partner_lazer_tech" model="res.partner">
|
||||
<field name="name">Lazer Tech</field>
|
||||
<field name="is_company">1</field>
|
||||
<field name="company_id" ref="base.main_company" />
|
||||
</record>
|
||||
</odoo>
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# © 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", group_operator="avg"
|
||||
)
|
||||
final_on_hand_qty = fields.Float(
|
||||
string="Forecasted Inventory", group_operator="avg"
|
||||
)
|
||||
to_procure = fields.Float(compute="_compute_to_procure", store=True)
|
||||
running_availability = fields.Float(
|
||||
string="Planned Availability",
|
||||
group_operator="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": "tree,form",
|
||||
"domain": domain,
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# © 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
|
||||
)
|
||||
|
|
@ -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"] = "[('id','in',%s)]" % self.mrp_production_ids.ids
|
||||
return result
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
# 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
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
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 name_get(self):
|
||||
return [
|
||||
(
|
||||
area.id,
|
||||
"[{}] {}".format(area.mrp_area_id.name, area.product_id.display_name),
|
||||
)
|
||||
for area in self
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _name_search(
|
||||
self, name, args=None, operator="ilike", limit=100, name_get_uid=None
|
||||
):
|
||||
if operator in ("ilike", "like", "=", "=like", "=ilike"):
|
||||
args = expression.AND(
|
||||
[
|
||||
args or [],
|
||||
[
|
||||
"|",
|
||||
"|",
|
||||
("product_id.name", operator, name),
|
||||
("product_id.default_code", operator, name),
|
||||
("mrp_area_id.name", operator, name),
|
||||
],
|
||||
]
|
||||
)
|
||||
return super(ProductMRPArea, self)._name_search(
|
||||
name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid
|
||||
)
|
||||
|
||||
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 = rec.product_id.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",
|
||||
"product_id.route_ids",
|
||||
"product_id.seller_ids",
|
||||
"location_proc_id",
|
||||
)
|
||||
def _compute_main_supplier(self):
|
||||
"""Simplified and similar to procurement.rule logic."""
|
||||
for rec in self:
|
||||
if rec.supply_method != "buy":
|
||||
rec.main_supplierinfo_id = False
|
||||
rec.main_supplier_id = False
|
||||
continue
|
||||
suppliers = rec.product_id.seller_ids.filtered(
|
||||
lambda r: (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
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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(StockQuant, self)._get_inventory_fields_write()
|
||||
return fields + ["product_uom_id", "inventory_quantity"]
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
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 > Master Data > Product MRP Area Parameters* and set
|
||||
the MRP parameters for a given product and area.
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
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>`_.
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
To manually run the MRP scheduler:
|
||||
|
||||
#. Go to *Manufacturing > Operations > Run MRP Multi Level*.
|
||||
#. On the wizard click *Run MRP*.
|
||||
|
||||
To launch replenishment orders (moves, purchases, production orders...):
|
||||
|
||||
#. Go to *Manufacturing > Operations > MRP Inventory*.
|
||||
#. Filter with *To procure*.
|
||||
#. Select multiple records and click on *Action > Procure* or click the right
|
||||
hand side gears in any record.
|
||||
#. On the wizard, check everything is ok and click *Execute*.
|
||||
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="group_change_mrp_procure_qty" model="res.groups">
|
||||
<field name="name">Change procure quantity in MRP</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
|
||||
<field name="category_id" ref="base.module_category_hidden" />
|
||||
</record>
|
||||
<record id="group_mrp_multi_level_run" model="res.groups">
|
||||
<field name="name">Run MRP Manually</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
|
||||
<field name="category_id" ref="base.module_category_hidden" />
|
||||
<field
|
||||
name="users"
|
||||
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
|
||||
/>
|
||||
</record>
|
||||
<record id="mrp_area_comp_rule" model="ir.rule">
|
||||
<field name="name">MRP Area multi-company rule</field>
|
||||
<field name="model_id" ref="model_mrp_area" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="mrp_product_mrp_area_comp_rule" model="ir.rule">
|
||||
<field name="name">Product MRP Area multi-company rule</field>
|
||||
<field name="model_id" ref="model_product_mrp_area" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="mrp_inventory_comp_rule" model="ir.rule">
|
||||
<field name="name">MRP Inventory multi-company rule</field>
|
||||
<field name="model_id" ref="model_mrp_inventory" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="mrp_move_comp_rule" model="ir.rule">
|
||||
<field name="name">MRP Move multi-company rule</field>
|
||||
<field name="model_id" ref="model_mrp_move" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="mrp_planned_order_comp_rule" model="ir.rule">
|
||||
<field name="name">MRP Planned Order multi-company rule</field>
|
||||
<field name="model_id" ref="model_mrp_planned_order" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
|
|
@ -0,0 +1,644 @@
|
|||
<!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:72f5c1e6f5fe8ddfe32ac230c0b2a6a6f9009a523c3e415ac2b05c21f9de08af
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<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/16.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-16-0/manufacture-16-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=16.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><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-6">13.0.1.5.0 (2020-04-09)</a></li>
|
||||
<li><a class="reference internal" href="#section-2" id="toc-entry-7">13.0.1.4.0 (2020-03-26)</a></li>
|
||||
<li><a class="reference internal" href="#section-3" id="toc-entry-8">13.0.1.3.0 (2020-03-02)</a></li>
|
||||
<li><a class="reference internal" href="#section-4" id="toc-entry-9">13.0.1.2.0 (2020-02-20)</a></li>
|
||||
<li><a class="reference internal" href="#section-5" id="toc-entry-10">13.0.1.1.0 (2020-02-21)</a></li>
|
||||
<li><a class="reference internal" href="#section-6" id="toc-entry-11">13.0.1.0.0 (2019-12-18)</a></li>
|
||||
<li><a class="reference internal" href="#section-7" id="toc-entry-12">12.0.1.0.0 (2019-08-05)</a></li>
|
||||
<li><a class="reference internal" href="#section-8" id="toc-entry-13">11.0.3.0.0 (2019-05-22)</a></li>
|
||||
<li><a class="reference internal" href="#section-9" id="toc-entry-14">11.0.2.2.0 (2019-05-02)</a></li>
|
||||
<li><a class="reference internal" href="#section-10" id="toc-entry-15">11.0.2.1.0 (2019-04-02)</a></li>
|
||||
<li><a class="reference internal" href="#section-11" id="toc-entry-16">11.0.2.0.0 (2018-11-20)</a></li>
|
||||
<li><a class="reference internal" href="#section-12" id="toc-entry-17">11.0.1.1.0 (2018-08-30)</a></li>
|
||||
<li><a class="reference internal" href="#section-13" id="toc-entry-18">11.0.1.0.1 (2018-08-03)</a></li>
|
||||
<li><a class="reference internal" href="#section-14" id="toc-entry-19">11.0.1.0.0 (2018-07-09)</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-20">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-21">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-22">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-23">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-24">Maintainers</a></li>
|
||||
</ul>
|
||||
</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 > Master Data > 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 > Operations > 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 > Operations > 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 class="section" id="section-1">
|
||||
<h3><a class="toc-backref" href="#toc-entry-6">13.0.1.5.0 (2020-04-09)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-7">13.0.1.4.0 (2020-03-26)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-8">13.0.1.3.0 (2020-03-02)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-9">13.0.1.2.0 (2020-02-20)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-10">13.0.1.1.0 (2020-02-21)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-11">13.0.1.0.0 (2019-12-18)</a></h3>
|
||||
<ul class="simple">
|
||||
<li>[MIG] Migration to v13.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-7">
|
||||
<h3><a class="toc-backref" href="#toc-entry-12">12.0.1.0.0 (2019-08-05)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-13">11.0.3.0.0 (2019-05-22)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-14">11.0.2.2.0 (2019-05-02)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-15">11.0.2.1.0 (2019-04-02)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-16">11.0.2.0.0 (2018-11-20)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-17">11.0.1.1.0 (2018-08-30)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-18">11.0.1.0.1 (2018-08-03)</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-19">11.0.1.0.0 (2018-07-09)</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Start of the history.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h2><a class="toc-backref" href="#toc-entry-20">Bug Tracker</a></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:%2016.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><a class="toc-backref" href="#toc-entry-21">Credits</a></h2>
|
||||
<div class="section" id="authors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-22">Authors</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Ucamco</li>
|
||||
<li>ForgeFlow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-23">Contributors</a></h3>
|
||||
<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">
|
||||
<h3><a class="toc-backref" href="#toc-entry-24">Maintainers</a></h3>
|
||||
<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> <a class="reference external image-reference" href="https://github.com/ChrisOForgeFlow"><img alt="ChrisOForgeFlow" src="https://github.com/ChrisOForgeFlow.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/manufacture/tree/16.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>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
from . import test_mrp_multi_level
|
||||
|
|
@ -0,0 +1,659 @@
|
|||
# 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",
|
||||
"type": "product",
|
||||
"list_price": 150.0,
|
||||
"produce_delay": 5.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",
|
||||
"type": "product",
|
||||
"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",
|
||||
"type": "product",
|
||||
"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",
|
||||
"type": "product",
|
||||
"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",
|
||||
"type": "product",
|
||||
"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",
|
||||
"type": "product",
|
||||
"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",
|
||||
"type": "product",
|
||||
"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,
|
||||
"produce_delay": 5.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",
|
||||
"type": "product",
|
||||
"tracking": "lot",
|
||||
"uom_id": cls.env.ref("uom.product_uom_unit").id,
|
||||
"list_price": 100.0,
|
||||
"produce_delay": 5.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_planned_start = date
|
||||
mo = mo_form.save()
|
||||
# Confirm the MO to generate stock moves:
|
||||
mo.action_confirm()
|
||||
return mo
|
||||
|
|
@ -0,0 +1,918 @@
|
|||
# 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_planned_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_11_special_scenario_1(self):
|
||||
"""When grouping demand supply and demand are in the same day but
|
||||
supply goes first."""
|
||||
moves = self.mrp_move_obj.search(
|
||||
[("product_id", "=", self.product_scenario_1.id)]
|
||||
)
|
||||
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_12_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_13_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_14_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_15_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_16_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_17_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")
|
||||
self.fp_4.route_ids = [
|
||||
(4, self.env.ref("purchase_stock.route_warehouse0_buy").id)
|
||||
]
|
||||
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_18_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):
|
||||
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_19_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_20_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):
|
||||
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_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):
|
||||
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_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):
|
||||
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(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):
|
||||
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_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):
|
||||
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_25_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)
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="mrp_area_tree">
|
||||
<field name="name">mrp.area.tree</field>
|
||||
<field name="model">mrp.area</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="warehouse_id" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="location_id" />
|
||||
<field name="calendar_id" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mrp_area_form">
|
||||
<field name="name">mrp.area.form</field>
|
||||
<field name="model">mrp.area</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MRP Area">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box" />
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Archived"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': [('active', '=', True)]}"
|
||||
/>
|
||||
<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" options="{'no_create': True}" />
|
||||
<field name="location_id" options="{'no_create': True}" />
|
||||
<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">tree,form</field>
|
||||
<field name="view_id" ref="mrp_area_tree" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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"
|
||||
attrs="{'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_tree" model="ir.ui.view">
|
||||
<field name="name">mrp.inventory.tree</field>
|
||||
<field name="model">mrp.inventory</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree 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
|
||||
attrs="{'invisible': [('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"
|
||||
attrs="{'invisible':[('to_procure','<=',0.0)]}"
|
||||
/>
|
||||
<field name="planned_order_ids" invisible="1" />
|
||||
<field name="supply_method" />
|
||||
<field name="main_supplier_id" optional="hide" />
|
||||
<field name="running_availability" />
|
||||
</tree>
|
||||
</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">tree,form,pivot,graph</field>
|
||||
<field name="view_id" ref="mrp_inventory_tree" />
|
||||
<field name="search_view_id" ref="mrp_inventory_search" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="mrp_move_view_tree" model="ir.ui.view">
|
||||
<field name="name">mrp.move.tree</field>
|
||||
<field name="model">mrp.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<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" />
|
||||
</tree>
|
||||
</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"
|
||||
attrs="{'invisible':[('mrp_origin', '!=', 'mo')]}"
|
||||
/>
|
||||
<field
|
||||
name="purchase_order_id"
|
||||
attrs="{'invisible':[('mrp_origin', '!=', 'po')]}"
|
||||
/>
|
||||
<field
|
||||
name="purchase_line_id"
|
||||
attrs="{'invisible':[('mrp_origin', '!=', 'po')]}"
|
||||
/>
|
||||
<field
|
||||
name="stock_move_id"
|
||||
attrs="{'invisible':[('mrp_origin', '!=', 'mv')]}"
|
||||
/>
|
||||
<field name="name" />
|
||||
<field name="origin" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="mrp_date" />
|
||||
<field
|
||||
name="current_date"
|
||||
attrs="{'invisible': [('current_date', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="state"
|
||||
attrs="{'invisible': [('state', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="mrp_order_number"
|
||||
attrs="{'invisible': [('mrp_order_number', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="parent_product_id"
|
||||
attrs="{'invisible': [('parent_product_id', '=', False)]}"
|
||||
/>
|
||||
<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">tree,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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_tree" model="ir.ui.view">
|
||||
<field name="name">mrp.planned.order.tree</field>
|
||||
<field name="model">mrp.planned.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree
|
||||
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" />
|
||||
</tree>
|
||||
</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"
|
||||
attrs="{'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">tree,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>
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="product_mrp_area_tree">
|
||||
<field name="name">product.mrp.area.tree</field>
|
||||
<field name="model">product.mrp.area</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<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" />
|
||||
</tree>
|
||||
</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"
|
||||
attrs="{'invisible': [('active', '=', True)]}"
|
||||
/>
|
||||
<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"
|
||||
attrs="{'invisible':[('supply_method', '!=', 'buy'), ('main_supplierinfo_id', '=', False)]}"
|
||||
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"
|
||||
attrs="{'invisible': [('supply_method', 'not in', ('pull', 'push', 'pull_push'))]}"
|
||||
/>
|
||||
<field name="mrp_lead_time" />
|
||||
<field
|
||||
name="main_supplierinfo_id"
|
||||
attrs="{'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">
|
||||
<tree>
|
||||
<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" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page
|
||||
name="planned_orders"
|
||||
string="Planned Orders"
|
||||
groups="base.group_no_one"
|
||||
>
|
||||
<field name="planned_order_ids" nolabel="1">
|
||||
<tree>
|
||||
<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" />
|
||||
</tree>
|
||||
</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">tree,form</field>
|
||||
<field name="view_id" ref="product_mrp_area_tree" />
|
||||
<field name="search_view_id" ref="product_mrp_area_search" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import mrp_multi_level
|
||||
from . import mrp_inventory_procure
|
||||
|
|
@ -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(MrpInventoryProcure, self).fields_view_get(
|
||||
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(MrpInventoryProcure, self).default_get(fields)
|
||||
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
|
||||
)
|
||||
|
|
@ -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">
|
||||
<tree nocreate="1" editable="top">
|
||||
<field name="mrp_inventory_id" invisible="True" />
|
||||
<field
|
||||
name="warehouse_id"
|
||||
groups="stock.group_stock_multi_locations"
|
||||
readonly="1"
|
||||
/>
|
||||
<field
|
||||
name="location_id"
|
||||
groups="stock.group_stock_multi_locations"
|
||||
readonly="1"
|
||||
/>
|
||||
<field name="product_id" readonly="1" />
|
||||
<field name="qty" />
|
||||
<field name="uom_id" groups="uom.group_uom" />
|
||||
<field name="date_planned" />
|
||||
<field name="supply_method" />
|
||||
</tree>
|
||||
</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>
|
||||
|
|
@ -0,0 +1,925 @@
|
|||
# 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"),
|
||||
}
|
||||
|
||||
@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 != "product"
|
||||
):
|
||||
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 = "Low level code 0 finished - Nbr. products: %s" % counter
|
||||
logger.info(log_msg)
|
||||
|
||||
while counter:
|
||||
llc += 1
|
||||
products = self.env["product.product"].search([("llc", "=", llc - 1)])
|
||||
p_templates = products.mapped("product_tmpl_id")
|
||||
bom_lines = self._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 = "Low level code {} finished - Nbr. products: {}".format(
|
||||
llc, 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", "=", "product")]
|
||||
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_domain, count=True)
|
||||
log_msg = "End Calculate MRP Applicable: %s" % 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: a.mrp_area_id == mrp_area
|
||||
):
|
||||
if self._exclude_from_mrp(product_mrp_area.product_id, mrp_area):
|
||||
continue
|
||||
init_counter += 1
|
||||
log_msg = "MRP Init: {} - {} ".format(
|
||||
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 = (
|
||||
"MRP Calculation LLC {} at {} Finished - Nbr. products: {}".format(
|
||||
llc, mrp_area.name, 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: 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
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?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>
|
||||
44
odoo-bringout-oca-manufacture-mrp_multi_level/pyproject.toml
Normal file
44
odoo-bringout-oca-manufacture-mrp_multi_level/pyproject.toml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-manufacture-mrp_multi_level"
|
||||
version = "16.0.0"
|
||||
description = "MRP Multi Level - Adds an MRP Scheduler"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-mrp>=16.0.0",
|
||||
"odoo-bringout-oca-manufacture-purchase_stock>=16.0.0",
|
||||
"odoo-bringout-oca-manufacture-mrp_warehouse_calendar>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["mrp_multi_level"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue