Skip to content

Commit b1adbe6

Browse files
[FIX]purchase_manual_delivery: pending in forecast
Purchase Order Lines that are pending to receive are not considered for stock forecast. Thus, the manual and automatic reordering of products can be done despite having already create a Purchase Order and surpassing the max quantity. Confirmed Purchase Orders with pending to receive Purchase Order Lines are now considered at the forecast and the forecast report.
1 parent 6fafbd6 commit b1adbe6

File tree

8 files changed

+191
-0
lines changed

8 files changed

+191
-0
lines changed

purchase_manual_delivery/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from . import models
2+
from . import report
23
from . import wizard

purchase_manual_delivery/__manifest__.py

+5
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@
1919
"views/purchase_order_views.xml",
2020
"views/res_config_view.xml",
2121
],
22+
"assets": {
23+
"web.assets_backend": [
24+
"purchase_manual_delivery/static/src/**/*",
25+
]
26+
},
2227
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import product_product
12
from . import res_company
23
from . import res_config
34
from . import purchase_order
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from odoo import models
2+
3+
4+
class ProductProduct(models.Model):
5+
_inherit = "product.product"
6+
7+
def _get_pending_lines_domain(self, location_ids=False, warehouse_ids=False):
8+
domain = self._get_lines_domain(location_ids, warehouse_ids)
9+
for domain_part in domain:
10+
if (
11+
isinstance(domain_part, tuple)
12+
and len(domain_part) == 3
13+
and domain_part[0] == "state"
14+
):
15+
domain_index = domain.index(domain_part)
16+
domain[domain_index] = ("state", "in", ("purchase", "done"))
17+
domain.insert(domain_index + 1, ("pending_to_receive", "=", True))
18+
domain.insert(domain_index, "&")
19+
break
20+
return domain
21+
22+
def _get_quantity_in_progress(self, location_ids=False, warehouse_ids=False):
23+
qty_by_product_location, qty_by_product_wh = super()._get_quantity_in_progress(
24+
location_ids, warehouse_ids
25+
)
26+
domain = self._get_pending_lines_domain(location_ids, warehouse_ids)
27+
groups = self.env["purchase.order.line"]._read_group(
28+
domain,
29+
[
30+
"product_id",
31+
"product_qty",
32+
"order_id",
33+
"product_uom",
34+
"orderpoint_id",
35+
"existing_qty",
36+
],
37+
["order_id", "product_id", "product_uom", "orderpoint_id"],
38+
lazy=False,
39+
)
40+
for group in groups:
41+
if group.get("orderpoint_id"):
42+
location = (
43+
self.env["stock.warehouse.orderpoint"]
44+
.browse(group["orderpoint_id"][:1])
45+
.location_id
46+
)
47+
else:
48+
order = self.env["purchase.order"].browse(group["order_id"][0])
49+
location = order.picking_type_id.default_location_dest_id
50+
product = self.env["product.product"].browse(group["product_id"][0])
51+
uom = self.env["uom.uom"].browse(group["product_uom"][0])
52+
product_qty = uom._compute_quantity(
53+
group["product_qty"], product.uom_id, round=False
54+
)
55+
remaining_qty = product_qty - group["existing_qty"]
56+
qty_by_product_location[(product.id, location.id)] += remaining_qty
57+
qty_by_product_wh[(product.id, location.warehouse_id.id)] += remaining_qty
58+
return qty_by_product_location, qty_by_product_wh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import stock_forecasted
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from odoo import models
2+
3+
4+
class ReplenishmentReport(models.AbstractModel):
5+
_inherit = "report.stock.report_product_product_replenishment"
6+
7+
def _serialize_docs(
8+
self, docs, product_template_ids=False, product_variant_ids=False
9+
):
10+
res = super()._serialize_docs(docs, product_template_ids, product_variant_ids)
11+
res["no_delivery_purchase_orders"] = docs["no_delivery_purchase_orders"].read(
12+
fields=["id", "name"]
13+
)
14+
return res
15+
16+
def _compute_draft_quantity_count(
17+
self, product_template_ids, product_variant_ids, wh_location_ids
18+
):
19+
res = super()._compute_draft_quantity_count(
20+
product_template_ids, product_variant_ids, wh_location_ids
21+
)
22+
domain = [
23+
("state", "in", ["purchase", "done"]),
24+
("pending_to_receive", "=", True),
25+
]
26+
domain += self._product_domain(product_template_ids, product_variant_ids)
27+
warehouse_id = self.env.context.get("warehouse", False)
28+
if warehouse_id:
29+
domain += [("order_id.picking_type_id.warehouse_id", "=", warehouse_id)]
30+
po_lines = self.env["purchase.order.line"].search(domain)
31+
in_sum = sum(po_lines.mapped(lambda po: po.product_qty - po.existing_qty))
32+
res["no_delivery_purchase_qty"] = in_sum
33+
res["no_delivery_purchase_orders"] = po_lines.mapped("order_id").sorted("name")
34+
res["qty"]["in"] += in_sum
35+
return res
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<templates id="template" xml:space="preserve">
3+
<t
4+
t-name="purchase_manual_delivery.ForecastedDetails"
5+
owl="1"
6+
t-inherit="stock.ForecastedDetails"
7+
t-inherit-mode="extension"
8+
>
9+
<xpath expr="//tr[@name='draft_po_in']" position="after">
10+
<tr t-if="props.docs.no_delivery_purchase_qty" name="no_delivery_in">
11+
<td colspan="2"> Requests without delivery</td>
12+
<td
13+
t-out="_formatFloat(props.docs.no_delivery_purchase_qty)"
14+
class="text-end"
15+
/>
16+
<td>
17+
<t
18+
t-foreach="props.docs.no_delivery_purchase_orders"
19+
t-as="purchase_order"
20+
t-key="purchase_order_index"
21+
>
22+
<t t-if="purchase_order_index > 0"> | </t>
23+
<a
24+
t-attf-href="#"
25+
t-out="purchase_order.name"
26+
class="fw-bold"
27+
t-on-click.prevent="() => this.props.openView('purchase.order', 'form', purchase_order.id)"
28+
/>
29+
</t>
30+
</td>
31+
</tr>
32+
</xpath>
33+
</t>
34+
</templates>

purchase_manual_delivery/tests/test_purchase_manual_delivery.py

+56
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,59 @@ def test_04_pending_to_receive(self):
301301

302302
# The PO Line should not be pending to receive
303303
self.assertFalse(po_existing_bigger.pending_to_receive)
304+
305+
def test_05_purchase_order_in_progress(self):
306+
"""
307+
Create a new Product and Purchase Order.
308+
Confirm Purchase Order and create a Picking with only a partial amount of
309+
the selected amount of the Purchase Order Line. Confirm the Picking.
310+
The quantity in progress is the pending to receive quantity of the Purchase
311+
Order Line.
312+
"""
313+
product_in_progress = self.env["product.product"].create(
314+
{
315+
"name": "Test product pending",
316+
"type": "product",
317+
"list_price": 1,
318+
"standard_price": 1,
319+
}
320+
)
321+
po_in_progress = self.purchase_order_obj.create(
322+
{
323+
"partner_id": self.ref("base.res_partner_3"),
324+
}
325+
)
326+
self.purchase_order_line_obj.create(
327+
{
328+
"order_id": po_in_progress.id,
329+
"product_id": product_in_progress.id,
330+
"product_uom": product_in_progress.uom_id.id,
331+
"name": product_in_progress.name,
332+
"price_unit": product_in_progress.standard_price,
333+
"date_planned": fields.datetime.now(),
334+
"product_qty": 5.0,
335+
}
336+
)
337+
po_in_progress.button_confirm_manual()
338+
location = self.env["stock.location"].browse(
339+
po_in_progress.picking_type_id.default_location_dest_id.id
340+
)
341+
wizard = (
342+
self.env["create.stock.picking.wizard"]
343+
.with_context(
344+
**{
345+
"active_model": "purchase.order",
346+
"active_id": po_in_progress.id,
347+
"active_ids": po_in_progress.ids,
348+
}
349+
)
350+
.create({})
351+
)
352+
wizard.fill_lines(po_in_progress.order_line)
353+
wizard.line_ids[0].qty = 2
354+
wizard.create_stock_picking()
355+
po_in_progress.picking_ids[0].button_validate()
356+
qty, _ = product_in_progress._get_quantity_in_progress(
357+
location_ids=location.ids
358+
)
359+
self.assertEqual(qty.get((product_in_progress.id, location.id)), 3)

0 commit comments

Comments
 (0)