Skip to content

Commit 00b6bc7

Browse files
[16.0][ADD] account_liquidity_forecast_purchase_stock
1 parent 0baee69 commit 00b6bc7

File tree

12 files changed

+851
-0
lines changed

12 files changed

+851
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
=========================================
2+
Account Liquidity Forecast Purchase Stock
3+
=========================================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:32303c30dc7d9ce2c82299004c2f99409472e2163d9b65b72098273c9a5c047a
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--reporting-lightgray.png?logo=github
20+
:target: https://github.com/OCA/account-financial-reporting/tree/16.0/account_liquidity_forecast_purchase_stock
21+
:alt: OCA/account-financial-reporting
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/account-financial-reporting-16-0/account-financial-reporting-16-0-account_liquidity_forecast_purchase_stock
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-reporting&target_branch=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module extends the functionality of account_liquidity_forecast_purchase by incorporating forecasted
32+
cash outflows derived from incoming stock moves associated with purchase orders.
33+
34+
It leverages the Expected Arrival Date (date) from each stock move line to determine when the cash outflow
35+
is expected to occur.
36+
37+
Additionally, you can still include draft purchase orders (RFQs) in the forecast by enabling the "Include draft PO"
38+
option in the report wizard. This way you get an overview of the RFQ and the draft and validated stock moves.
39+
40+
To access the Liquidity Forecast report, go to:
41+
42+
Accounting/Invoicing > Reporting > Liquidity Forecast
43+
44+
**Table of contents**
45+
46+
.. contents::
47+
:local:
48+
49+
Bug Tracker
50+
===========
51+
52+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_.
53+
In case of trouble, please check there if your issue has already been reported.
54+
If you spotted it first, help us to smash it by providing a detailed and welcomed
55+
`feedback <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20account_liquidity_forecast_purchase_stock%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
56+
57+
Do not contact contributors directly about support or help with technical issues.
58+
59+
Credits
60+
=======
61+
62+
Authors
63+
~~~~~~~
64+
65+
* ForgeFlow
66+
67+
Contributors
68+
~~~~~~~~~~~~
69+
70+
* `ForgeFlow <https://www.forgeflow.com>`__:
71+
72+
* Mateu Griful
73+
74+
Maintainers
75+
~~~~~~~~~~~
76+
77+
This module is maintained by the OCA.
78+
79+
.. image:: https://odoo-community.org/logo.png
80+
:alt: Odoo Community Association
81+
:target: https://odoo-community.org
82+
83+
OCA, or the Odoo Community Association, is a nonprofit organization whose
84+
mission is to support the collaborative development of Odoo features and
85+
promote its widespread use.
86+
87+
This module is part of the `OCA/account-financial-reporting <https://github.com/OCA/account-financial-reporting/tree/16.0/account_liquidity_forecast_purchase_stock>`_ project on GitHub.
88+
89+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import report
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com)
2+
{
3+
"name": "Account Liquidity Forecast Purchase Stock",
4+
"version": "16.0.1.0.0",
5+
"category": "Reporting",
6+
"summary": "Account Liquidity Forecast Purchase Stock",
7+
"author": "ForgeFlow," "Odoo Community Association (OCA)",
8+
"website": "https://github.com/OCA/account-financial-reporting",
9+
"depends": ["account_liquidity_forecast_purchase", "purchase_stock"],
10+
"data": [],
11+
"installable": True,
12+
"application": True,
13+
"auto_install": False,
14+
"license": "AGPL-3",
15+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
* `ForgeFlow <https://www.forgeflow.com>`__:
2+
3+
* Mateu Griful
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
This module extends the functionality of account_liquidity_forecast_purchase by incorporating forecasted
2+
cash outflows derived from incoming stock moves associated with purchase orders.
3+
4+
It leverages the Expected Arrival Date (date) from each stock move line to determine when the cash outflow
5+
is expected to occur.
6+
7+
Additionally, you can still include draft purchase orders (RFQs) in the forecast by enabling the "Include draft PO"
8+
option in the report wizard. This way you get an overview of the RFQ and the draft and validated stock moves.
9+
10+
To access the Liquidity Forecast report, go to:
11+
12+
Accounting/Invoicing > Reporting > Liquidity Forecast
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import liquidity_forecast
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com)
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
# account_liquidity_forecast_purchase_stock/report/liquidity_forecast.py
4+
from odoo import _, models
5+
6+
7+
class LiquidityForecastReport(models.AbstractModel):
8+
_inherit = "report.account_liquidity_forecast.liquidity_forecast"
9+
10+
def _prepare_liquidity_forecast_lines_period(
11+
self, data, liquidity_forecast_lines, period, periods
12+
):
13+
res = super()._prepare_liquidity_forecast_lines_period(
14+
data, liquidity_forecast_lines, period, periods
15+
)
16+
self._prepare_cash_flow_lines_po_moves(
17+
data, liquidity_forecast_lines, period, periods
18+
)
19+
return res
20+
21+
def _prepare_cash_flow_lines_po_moves(
22+
self, data, liquidity_forecast_lines, period, periods
23+
):
24+
"""Add stock moves to the liquidity forecast"""
25+
domain_move_states = ["confirmed", "waiting", "assigned"]
26+
27+
stock_moves = self.env["stock.move"].search(
28+
[
29+
("state", "in", domain_move_states),
30+
("purchase_line_id", "!=", False),
31+
("company_id", "=", data["company_id"]),
32+
]
33+
)
34+
35+
code = "cash_flow_line_out_purchase_moves"
36+
lines = [line for line in liquidity_forecast_lines if line["code"] == code]
37+
if lines:
38+
flow_line = lines[0]
39+
# RESET amounts before recomputing
40+
for p_seq in flow_line["periods"]:
41+
flow_line["periods"][p_seq]["amount"] = 0.0
42+
flow_line["periods"][p_seq]["domain"] = ""
43+
else:
44+
flow_line = {
45+
"code": code,
46+
"type": "amount",
47+
"level": "detail",
48+
"model": "stock.move",
49+
"title": _("Expected PO Deliveries"),
50+
"sequence": 3550,
51+
"periods": {
52+
p["sequence"]: {"amount": 0.0, "domain": ""} for p in periods
53+
},
54+
}
55+
liquidity_forecast_lines.append(flow_line)
56+
57+
move_ids_by_period = {p["sequence"]: [] for p in periods}
58+
59+
for move in stock_moves:
60+
if not move.date:
61+
continue
62+
move_date = move.date.date() if hasattr(move.date, "date") else move.date
63+
64+
for p in periods:
65+
if p["date_from"] <= move_date <= p["date_to"]:
66+
po_line = move.purchase_line_id
67+
price = po_line.price_unit
68+
qty = move.product_qty
69+
flow_line["periods"][p["sequence"]]["amount"] += -qty * price
70+
move_ids_by_period[p["sequence"]].append(move.id)
71+
break
72+
73+
for seq, move_ids in move_ids_by_period.items():
74+
flow_line["periods"][seq]["domain"] = [("id", "in", move_ids)]
75+
76+
def _prepare_cash_flow_lines_po_lines(
77+
self, data, liquidity_forecast_lines, period, periods
78+
):
79+
"""Override the purchase module's PO lines method to exclude lines
80+
covered by stock moves"""
81+
# Get PO lines using the filtering logic
82+
po_lines = self.get_po_lines(data, period, periods)
83+
84+
if not po_lines:
85+
return
86+
87+
code = "cash_flow_line_out_purchase"
88+
lines = [line for line in liquidity_forecast_lines if line["code"] == code]
89+
if lines:
90+
flow_line = lines[0]
91+
# RESET amounts before recomputing
92+
for p_seq in flow_line["periods"]:
93+
flow_line["periods"][p_seq]["amount"] = 0.0
94+
flow_line["periods"][p_seq]["domain"] = ""
95+
else:
96+
flow_line = {
97+
"code": code,
98+
"type": "amount",
99+
"level": "detail",
100+
"model": "purchase.order.line",
101+
"title": _("Purchase Orders"),
102+
"sequence": 3500,
103+
"periods": {
104+
p["sequence"]: {"amount": 0.0, "domain": ""} for p in periods
105+
},
106+
}
107+
liquidity_forecast_lines.append(flow_line)
108+
109+
po_line_ids_by_period = {p["sequence"]: [] for p in periods}
110+
111+
for po_line in po_lines:
112+
if not po_line.date_planned:
113+
continue
114+
115+
planned_date = (
116+
po_line.date_planned.date()
117+
if hasattr(po_line.date_planned, "date")
118+
else po_line.date_planned
119+
)
120+
121+
for p in periods:
122+
if p["date_from"] <= planned_date <= p["date_to"]:
123+
# Calculate uninvoiced amount
124+
valid_invoice_lines = po_line.invoice_lines.filtered(
125+
lambda inv_line: inv_line.move_id.state
126+
not in ("draft", "cancel")
127+
)
128+
invoiced_qty = sum(valid_invoice_lines.mapped("quantity"))
129+
uninvoiced_qty = po_line.product_qty - invoiced_qty
130+
131+
if uninvoiced_qty > 0:
132+
amount = -uninvoiced_qty * po_line.price_unit
133+
flow_line["periods"][p["sequence"]]["amount"] += amount
134+
po_line_ids_by_period[p["sequence"]].append(po_line.id)
135+
break
136+
137+
for seq, po_line_ids in po_line_ids_by_period.items():
138+
flow_line["periods"][seq]["domain"] = [("id", "in", po_line_ids)]
139+
140+
def get_po_lines(self, data, period, periods):
141+
"""Filter PO lines to exclude those already covered by stock moves"""
142+
states = ["sent", "to approve", "purchase", "done"]
143+
if data.get("include_po_draft", False):
144+
states.append("draft")
145+
146+
domain = [
147+
("state", "in", states),
148+
("date_planned", "<=", period["date_to"]),
149+
]
150+
if period["sequence"] > 0:
151+
domain += [("date_planned", ">=", period["date_from"])]
152+
153+
po_lines = self.env["purchase.order.line"].search(domain)
154+
155+
filtered_po_lines = []
156+
157+
for line in po_lines:
158+
# Check if PO line is already covered by moves
159+
covered_by_move = False
160+
for move in line.move_ids.filtered(
161+
lambda m: m.state not in ("done", "cancel")
162+
):
163+
if not move.date:
164+
continue
165+
move_date = (
166+
move.date.date() if hasattr(move.date, "date") else move.date
167+
)
168+
for p in periods:
169+
if p["date_from"] <= move_date <= p["date_to"]:
170+
covered_by_move = True
171+
break
172+
if covered_by_move:
173+
break
174+
175+
if covered_by_move:
176+
continue # already handled in _prepare_cash_flow_lines_po_moves
177+
178+
# Check if there's uninvoiced quantity
179+
valid_invoice_lines = line.invoice_lines.filtered(
180+
lambda inv_line: inv_line.move_id.state not in ("draft", "cancel")
181+
)
182+
invoiced_qty = sum(valid_invoice_lines.mapped("quantity"))
183+
184+
if line.product_qty > invoiced_qty:
185+
filtered_po_lines.append(line)
186+
187+
return self.env["purchase.order.line"].browse(
188+
[line.id for line in filtered_po_lines]
189+
)

0 commit comments

Comments
 (0)