[12.0][ADD] stock_pull_list

This commit is contained in:
Lois Rilo
2020-04-02 15:45:03 +02:00
committed by Mateu Griful
parent 18b7281d05
commit 5e8106c2e1
15 changed files with 1158 additions and 0 deletions

107
stock_pull_list/README.rst Normal file
View File

@@ -0,0 +1,107 @@
===============
Stock Pull List
===============
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |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%2Fstock--logistics--warehouse-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_pull_list
:alt: OCA/stock-logistics-warehouse
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-12-0/stock-logistics-warehouse-12-0-stock_pull_list
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/153/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
The pull list checks the stock situation at the given location and calculates
the shortfall quantities (quantity needed to cover all needs) for products.
Procurements can be created for these shortfall quantities.
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Usage
=====
To use the module follow the next steps:
#. Go to *Inventory > Operations > Generate Pull List*.
#. Select the location to get the pull list from. Add some filtering if needed.
#. Click on Prepare. You will now see the pull list with all the needs.
#. Adjust grouping options as needed. This will generate different procurement
groups.
#. Click on *Procure*.
Known issues / Roadmap
======================
* In wizard, when `exclude_reserved` is selected, handle partially available moves.
* Use sequence numbering for procurement groups made from pull list.
* Return a pull list summary at the end.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_pull_list%0Aversion:%2012.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
~~~~~~~
* ForgeFlow
Contributors
~~~~~~~~~~~~
* Lois Rilo <lois.rilo@forgeflow.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-LoisRForgeFlow| image:: https://github.com/LoisRForgeFlow.png?size=40px
:target: https://github.com/LoisRForgeFlow
:alt: LoisRForgeFlow
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-LoisRForgeFlow|
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_pull_list>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -0,0 +1,24 @@
# Copyright 2020 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
{
"name": "Stock Pull List",
"summary": "The pull list checks the stock situation and calculates "
"needed quantities.",
"version": "12.0.1.0.0",
"license": "LGPL-3",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"author": "ForgeFlow, "
"Odoo Community Association (OCA)",
"maintainers": ["LoisRForgeFlow"],
"development_status": "Alpha",
"category": "Warehouse Management",
"depends": [
"stock",
"stock_available_unreserved",
],
"data": [
"wizards/stock_pull_list_wizard.xml",
],
"installable": True,
}

View File

@@ -0,0 +1 @@
* Lois Rilo <lois.rilo@forgeflow.com>

View File

@@ -0,0 +1,3 @@
The pull list checks the stock situation at the given location and calculates
the shortfall quantities (quantity needed to cover all needs) for products.
Procurements can be created for these shortfall quantities.

View File

@@ -0,0 +1,3 @@
* In wizard, when `exclude_reserved` is selected, handle partially available moves.
* Use sequence numbering for procurement groups made from pull list.
* Return a pull list summary at the end.

View File

@@ -0,0 +1,8 @@
To use the module follow the next steps:
#. Go to *Inventory > Operations > Generate Pull List*.
#. Select the location to get the pull list from. Add some filtering if needed.
#. Click on Prepare. You will now see the pull list with all the needs.
#. Adjust grouping options as needed. This will generate different procurement
groups.
#. Click on *Procure*.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,451 @@
<?xml version="1.0" encoding="utf-8" ?>
<!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 0.15.2: http://docutils.sourceforge.net/" />
<title>Stock Pull List</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/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: grey; } /* 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 {
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="stock-pull-list">
<h1 class="title">Stock Pull List</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" 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" href="https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_pull_list"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-12-0/stock-logistics-warehouse-12-0-stock_pull_list"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/153/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>The pull list checks the stock situation at the given location and calculates
the shortfall quantities (quantity needed to cover all needs) for products.
Procurements can be created for these shortfall quantities.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>To use the module follow the next steps:</p>
<ol class="arabic simple">
<li>Go to <em>Inventory &gt; Operations &gt; Generate Pull List</em>.</li>
<li>Select the location to get the pull list from. Add some filtering if needed.</li>
<li>Click on Prepare. You will now see the pull list with all the needs.</li>
<li>Adjust grouping options as needed. This will generate different procurement
groups.</li>
<li>Click on <em>Procure</em>.</li>
</ol>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id2">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>In wizard, when <cite>exclude_reserved</cite> is selected, handle partially available moves.</li>
<li>Use sequence numbering for procurement groups made from pull list.</li>
<li>Return a pull list summary at the end.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/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 smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_pull_list%0Aversion:%2012.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">
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>ForgeFlow</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
<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">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/LoisRForgeFlow"><img alt="LoisRForgeFlow" src="https://github.com/LoisRForgeFlow.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/tree/12.0/stock_pull_list">OCA/stock-logistics-warehouse</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>
</body>
</html>

View File

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

View File

@@ -0,0 +1,79 @@
# Copyright 2020 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo.tests.common import TransactionCase
from odoo import fields
from datetime import timedelta as td
class TestPullListCommon(TransactionCase):
def setUp(self):
super().setUp()
self.wh_obj = self.env["stock.warehouse"]
self.move_obj = self.env["stock.move"]
self.picking_obj = self.env["stock.picking"]
self.wiz_obj = self.env["stock.pull.list.wizard"]
self.company = self.env.ref("base.main_company")
self.warehouse = self.env.ref("stock.warehouse0")
self.customer_loc = self.env.ref("stock.stock_location_customers")
self.warehouse_2 = self.wh_obj.create({
"code": "WH-T",
"name": "Warehouse Test",
})
self.product_a = self.env["product.product"].create({
"name": "test product A",
"default_code": "TEST-A",
"type": "product",
})
route_vals = {
"name": "WH2 -> WH",
}
self.transfer_route = self.env["stock.location.route"].create(
route_vals)
rule_vals = {
"location_id": self.warehouse.lot_stock_id.id,
"location_src_id": self.warehouse_2.lot_stock_id.id,
"action": "pull_push",
"warehouse_id": self.warehouse.id,
"propagate_warehouse_id": self.warehouse_2.id,
"picking_type_id": self.env.ref("stock.picking_type_internal").id,
"name": "WH2->WH",
"route_id": self.transfer_route.id,
"delay": 1,
}
self.transfer_rule = self.env["stock.rule"].create(rule_vals)
self.product_a.route_ids = [(6, 0, self.transfer_route.ids)]
# Dates:
self.today = fields.Datetime.today()
self.yesterday = self.today - td(days=1)
self.date_3 = self.today + td(days=3)
def _generate_moves(self):
self.create_picking_out_a(self.yesterday, 50)
self.create_picking_out_a(self.date_3, 70)
def create_picking_out_a(self, date_move, qty):
picking = self.picking_obj.create({
"picking_type_id": self.ref("stock.picking_type_out"),
"location_id": self.warehouse.lot_stock_id.id,
"location_dest_id": self.customer_loc.id,
"move_lines": [
(0, 0, {
"name": "Test move",
"product_id": self.product_a.id,
"date_expected": date_move,
"date": date_move,
"product_uom": self.product_a.uom_id.id,
"product_uom_qty": qty,
"location_id": self.warehouse.lot_stock_id.id,
"location_dest_id": self.customer_loc.id,
})]
})
picking.action_confirm()
return picking

View File

@@ -0,0 +1,37 @@
# Copyright 2020 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from .common import TestPullListCommon
class TestStockPullList(TestPullListCommon):
def test_01_default_options(self):
self._generate_moves()
wiz = self.wiz_obj.create({})
wiz.action_prepare()
lines = wiz.line_ids.filtered(lambda l: l.product_id == self.product_a)
self.assertEqual(len(lines), 2)
line_1 = lines.filtered(
lambda l: l.date_expected == self.yesterday.date())
self.assertEqual(line_1.raw_demand_qty, 50)
self.assertEqual(line_1.needed_qty, 50)
self.assertEqual(line_1.stock_rule_id, self.transfer_rule)
line_2 = lines.filtered(
lambda l: l.date_expected == self.date_3.date())
self.assertEqual(line_2.raw_demand_qty, 70)
self.assertEqual(line_2.needed_qty, 70)
def test_02_consolidate(self):
self._generate_moves()
wiz = self.wiz_obj.create({
"consolidate_by_product": True,
})
wiz.action_prepare()
line = wiz.line_ids.filtered(lambda l: l.product_id == self.product_a)
self.assertEqual(len(line), 1)
self.assertEqual(line.date_expected, self.today.date())
expected = 50 + 70
self.assertEqual(line.raw_demand_qty, expected)
self.assertEqual(line.needed_qty, expected)

View File

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

View File

@@ -0,0 +1,334 @@
# Copyright 2020 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
import itertools
class PullListWizard(models.TransientModel):
_name = "stock.pull.list.wizard"
_description = "Stock Pull List Wizard"
@api.model
def default_get(self, fields):
res = super().default_get(fields)
company = self.env.user.company_id
wh = self.env["stock.warehouse"].search(
[("company_id", "=", company.id)], limit=1)
res.update({
"warehouse_id": wh.id,
"location_id": wh.lot_stock_id.id,
})
return res
location_id = fields.Many2one(
comodel_name="stock.location",
required=True,
)
warehouse_id = fields.Many2one(
comodel_name="stock.warehouse",
)
line_ids = fields.One2many(
comodel_name="stock.pull.list.wizard.line",
inverse_name="wizard_id",
readonly=True,
)
# Step 1 - filtering options.
exclude_reserved = fields.Boolean()
location_dest_id = fields.Many2one(
string="Destination Location",
comodel_name="stock.location",
)
date_to = fields.Date()
consolidate_by_product = fields.Boolean(
help="All needs for each product will be grouped in one line, "
"disregarding date.",
)
procurement_group_ids = fields.Many2many(
comodel_name="procurement.group"
)
# Step 2 - filtering options.
select_all = fields.Boolean(default=True)
rule_action = fields.Selection(
selection=lambda self: self.env["stock.rule"]._fields["action"].selection,
)
available_in_source_location = fields.Boolean(
help="Select only rules with enough available stock in source "
"location. Applies for rules with a source location.",
)
# Step 2 - grouping options.
max_lines = fields.Integer()
group_by_rule = fields.Boolean()
def _get_moves_demand_domain(self):
self.ensure_one()
domain = [
("location_id", "child_of", self.location_id.id),
("state", "not in", ("draft", "done", "cancel")),
]
if self.location_dest_id:
domain.append(("location_dest_id", "=", self.location_dest_id.id))
if self.exclude_reserved:
domain.append(("state", "not in", ("assigned",)))
if self.date_to:
domain.append(("date_expected", "<=", self.date_to))
if self.procurement_group_ids:
domain.append(("group_id", "in", self.procurement_group_ids.ids))
return domain
def _get_moves_incoming_domain(self):
self.ensure_one()
domain = [
("location_dest_id", "child_of", self.location_id.id),
("state", "not in", ("draft", "done", "cancel")),
]
if self.date_to:
domain.append(("date_expected", "<=", self.date_to))
return domain
@api.model
def _prepare_line_values(self, key, demand_qty, supply_qty):
product, location, date_expected = key
rule = self._get_stock_rule_id(product, location)
global qty_assigned
prev = qty_assigned.setdefault(product, 0.0)
qty_available = self._get_available_qty(product, location) - prev
need_without_stock = max(demand_qty - supply_qty, 0.0)
qty_assigned_now = min(qty_available, need_without_stock)
qty_needed = max(demand_qty - qty_available - supply_qty, 0.0)
qty_assigned[product] = prev + qty_assigned_now
return {
"product_id": product,
"location_id": location,
"date_expected": date_expected,
"stock_rule_id": rule.id if rule else False,
"raw_demand_qty": demand_qty,
"available_qty": qty_available,
"incoming_qty": supply_qty,
"needed_qty": qty_needed,
}
def _get_available_qty(self, product, location):
product_obj = self.env["product.product"]
product_l = product_obj.with_context(
{"location": location.id}).browse(product.id)
if self.exclude_reserved:
return product_l.qty_available_not_res
return product_l.qty_available
@api.model
def _get_stock_rule_id(self, product_id, location_id):
values = {
"warehouse_id": self.warehouse_id,
"company_id": self.env.user.company_id.id,
}
stock_rule_id = self.env["procurement.group"]._get_rule(
product_id, location_id, values)
return stock_rule_id
def action_prepare(self):
domain = self._get_moves_demand_domain()
# `read_group` is not possible here because of the date format the
# method returns.
demand_moves = self.env["stock.move"].search(
domain, order="date_expected asc")
demand_dict = {}
force_date = fields.Date.today() if self.consolidate_by_product \
else False
for demand in demand_moves:
key = (
demand.product_id, demand.location_id,
fields.Date.to_date(demand.date_expected)
if not force_date else force_date,
)
prev = demand_dict.setdefault(key, 0.0)
# TODO: when exclude_reserved is selected, handle partially avail.
demand_dict[key] = prev + demand.product_uom_qty
domain = self._get_moves_incoming_domain()
incoming_moves = self.env["stock.move"].search(
domain, order="date_expected asc")
incoming_dict = {}
for supply in incoming_moves:
move_for_date = demand_moves.filtered(
lambda m: m.product_id == supply.product_id and
m.date_expected >= supply.date_expected)
if move_for_date:
date_selected = move_for_date[0].date_expected \
if not force_date else force_date
else:
# Supply is later than last demand -> ignore it.
continue
key = (
supply.product_id, supply.location_dest_id,
fields.Date.to_date(date_selected),
)
prev = incoming_dict.setdefault(key, 0.0)
incoming_dict[key] = prev + supply.product_uom_qty
lines = []
global qty_assigned
qty_assigned = {}
for key, demand_qty in demand_dict.items():
supply_qty = incoming_dict.get(key, 0.0)
lines.append((0, 0, self._prepare_line_values(
key, demand_qty, supply_qty)))
self.update({
"line_ids": lines,
})
res = self._act_window_pull_list_step_2()
return res
def _act_window_pull_list_step_2(self):
view_id = self.env.ref(
"stock_pull_list.view_run_stock_pull_list_wizard_wizard_step_2").id
res = {
"name": _("Pull List"),
"src_model": "stock.pull.list.wizard",
"view_type": "form",
"view_mode": "form",
"view_id": view_id,
"target": "new",
"res_model": "stock.pull.list.wizard",
"res_id": self.id,
"type": "ir.actions.act_window",
}
return res
def action_update_selected(self):
for line in self.line_ids:
if self.select_all:
line.selected = True
continue
rule_invalid = self.rule_action and \
self.rule_action != line.stock_rule_id.action
if self.available_in_source_location:
available = line._is_available_in_source_location()
else:
available = True
if rule_invalid or not available:
line.selected = False
else:
line.selected = True
# The wizard must be reloaded in order to show the new product lines
res = self._act_window_pull_list_step_2()
return res
def _prepare_procurement_values(self, date, group):
values = {
"date_planned": date,
"warehouse_id": self.warehouse_id,
"company_id": self.env.user.company_id,
"group_id": group,
}
return values
def _get_fields_for_keys(self):
fields = []
if self.group_by_rule:
fields.append("stock_rule_id")
return fields
def _get_procurement_group_keys(self):
fields = self._get_fields_for_keys()
if not fields:
return [False]
options_list = []
for f in fields:
# Many2many only field type supported. As more needs arise, this
# can be extended
options_list.append(self.line_ids.mapped(f).ids)
return list(itertools.product(*options_list))
def _prepare_proc_group_values(self):
# TODO: use special secuence to name procurement groups of pull lists.
return {}
def action_procure(self):
self.ensure_one()
lines_obj = self.env["stock.pull.list.wizard.line"]
errors = []
proc_groups = []
# User requesting the procurement is passed by context to be able to
# update final MO, PO or trasfer with that information.
# TODO: migration to v13: requested_uid is not needed.
pg_obj = self.env["procurement.group"].with_context(
requested_uid=self.env.user)
grouping_keys = self._get_procurement_group_keys()
fields = self._get_fields_for_keys()
for gk in grouping_keys:
domain = [("wizard_id", "=", self.id), ("needed_qty", ">", 0.0)]
for i, f in enumerate(fields):
domain.append((f, "=", gk[i]))
n = 0
lines = lines_obj.search(domain)
if not lines:
continue
group = pg_obj.create(self._prepare_proc_group_values())
proc_groups.append(group.id)
for line in lines.filtered(lambda l: l.selected):
n += 1
if 0 < self.max_lines < n:
n = 0
group = pg_obj.create(self._prepare_proc_group_values())
proc_groups.append(group.id)
values = self._prepare_procurement_values(
line.date_expected, group)
try:
pg_obj.run(
line.product_id,
line.needed_qty,
line.product_id.uom_id,
line.location_id,
"Pull List %s" % self.id,
"Pull List %s" % self.id,
values
)
except UserError as error:
errors.append(error.name)
if errors:
raise UserError("\n".join(errors))
res = {
"name": _("Generated Procurement Groups"),
"src_model": "stock.pull.list.wizard",
"view_type": "form",
"view_mode": "tree,form",
"res_model": "procurement.group",
"type": "ir.actions.act_window",
"domain": str([("id", "in", proc_groups)]),
}
return res
class PullListWizardLine(models.TransientModel):
_name = "stock.pull.list.wizard.line"
_description = "Stock Pull List Wizard Line"
wizard_id = fields.Many2one(
comodel_name="stock.pull.list.wizard",
)
product_id = fields.Many2one(
comodel_name="product.product",
)
location_id = fields.Many2one(
comodel_name="stock.location",
)
date_expected = fields.Date()
available_qty = fields.Float()
incoming_qty = fields.Float()
raw_demand_qty = fields.Float()
needed_qty = fields.Float()
stock_rule_id = fields.Many2one(
comodel_name="stock.rule",
)
selected = fields.Boolean(default=True)
def _is_available_in_source_location(self):
if not self.stock_rule_id.location_src_id:
return False
qty_avail = self.wizard_id._get_available_qty(
self.product_id, self.stock_rule_id.location_src_id)
return qty_avail > self.needed_qty

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 ForgeFlow S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="view_run_stock_pull_list_wizard_wizard" model="ir.ui.view">
<field name="name">stock.pull.list.wizard.form</field>
<field name="model">stock.pull.list.wizard</field>
<field name="arch" type="xml">
<form>
<p>The pull list checks the stock situation at the given location and calculates
the shortfall quantities (quantity needed to cover all needs) for products.</p>
<group>
<field name="warehouse_id" options="{'no_create': True}"/>
<field name="location_id" options="{'no_create': True}"/>
</group>
<p>All existing Stock moves moving outside of the location specified will be considered demand.
You can filter these moves in the section below.</p>
<group name="options" string="Filtering">
<group>
<field name="date_to"/>
<field name="location_dest_id" options="{'no_create': True}"/>
<field name="procurement_group_ids" widget="many2many_tags" options="{'no_create': True}"/>
</group>
<group>
<field name="exclude_reserved"/>
<field name="consolidate_by_product"/>
</group>
</group>
<footer>
<button name="action_prepare" string="Prepare" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="view_run_stock_pull_list_wizard_wizard_step_2" model="ir.ui.view">
<field name="name">stock.pull.list.wizard.form.2</field>
<field name="model">stock.pull.list.wizard</field>
<field name="arch" type="xml">
<form>
<group string="Needs">
<field name="line_ids" readonly="1" nolabel="1">
<tree>
<field name="wizard_id" invisible="1"/>
<field name="product_id"/>
<field name="location_id"/>
<field name="date_expected"/>
<field name="available_qty"/>
<field name="incoming_qty"/>
<field name="raw_demand_qty"/>
<field name="needed_qty"/>
<field name="stock_rule_id"/>
<field name="selected" widget="toggle_button"/>
</tree>
</field>
</group>
<group string="Filter Selected">
<group>
<field name="consolidate_by_product" invisible="1"/>
<field name="select_all"/>
<field name="rule_action" attrs="{'invisible':[('select_all', '!=', False)]}"/>
<field name="available_in_source_location"
attrs="{'invisible':['|', ('consolidate_by_product', '!=', True), ('select_all', '!=', False)]}"/>
<field name="exclude_reserved"
attrs="{'invisible':['|', ('available_in_source_location', '!=', True), ('select_all', '!=', False)]}"/>
</group>
<group>
<button name="action_update_selected" string="Apply Filter" type="object" icon="fa-cogs"/>
</group>
</group>
<group name="grouping" string="Split/Grouping Options">
<group>
<field name="max_lines"/>
</group>
<group>
<field name="group_by_rule"/>
</group>
</group>
<footer>
<button name="action_procure" string="Procure" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<act_window name="Generate Pull List"
res_model="stock.pull.list.wizard"
src_model="stock.pull.list.wizard"
view_mode="form"
target="new"
key2="client_action_multi"
id="action_stock_pull_list_wizard"
/>
<menuitem name="Generate Pull List"
id="menu_stock_pull_list_wizard"
action="action_stock_pull_list_wizard"
parent="stock.menu_stock_warehouse_mgmt"
groups="stock.group_stock_manager"
sequence="90"
/>
</odoo>