forked from OCA/stock-logistics-workflow
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhooks.py
344 lines (317 loc) · 14.5 KB
/
hooks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# Copyright 2020 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from collections import defaultdict
from datetime import datetime
from odoo import _
from odoo.tools import float_is_zero
from odoo.addons.stock_account.models.product import ProductProduct
from odoo.addons.stock_account.models.stock_move import StockMove
# flake8: noqa: C901
def post_load_hook():
def _run_fifo_new(self, quantity, company):
if not hasattr(self, "_run_fifo_prepare_candidate_update"):
return self._run_fifo_original(quantity, company)
# Find back incoming stock valuation layers (called candidates here)
# to value `quantity`.
qty_to_take_on_candidates = quantity
candidates = self._get_fifo_candidates(company)
new_standard_price = 0
tmp_value = 0 # to accumulate the value taken on the candidates
taken_data = {}
for candidate in candidates:
qty_taken_on_candidate = self._get_qty_taken_on_candidate(
qty_to_take_on_candidates, candidate
)
taken_data[candidate.id] = {"quantity": qty_taken_on_candidate}
candidate_unit_cost = candidate.remaining_value / candidate.remaining_qty
new_standard_price = candidate_unit_cost
value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost
value_taken_on_candidate = candidate.currency_id.round(
value_taken_on_candidate
)
taken_data[candidate.id].update(
{
"value": value_taken_on_candidate,
}
)
new_remaining_value = candidate.remaining_value - value_taken_on_candidate
candidate_vals = {
"remaining_qty": candidate.remaining_qty - qty_taken_on_candidate,
"remaining_value": new_remaining_value,
}
# Start Hook Prepare Candidate
candidate_vals = self._run_fifo_prepare_candidate_update(
candidate,
qty_taken_on_candidate,
value_taken_on_candidate,
candidate_vals,
)
# End Hook Prepare Candidate
candidate.write(candidate_vals)
qty_to_take_on_candidates -= qty_taken_on_candidate
tmp_value += value_taken_on_candidate
if float_is_zero(
qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding
):
if float_is_zero(
candidate.remaining_qty, precision_rounding=self.uom_id.rounding
):
next_candidates = candidates.filtered(
lambda svl: svl.remaining_qty > 0
)
new_standard_price = (
next_candidates
and next_candidates[0].unit_cost
or new_standard_price
)
break
# Update the standard price with the price of the last used candidate,
# if any.
# START HOOK update standard price
if hasattr(self, "_price_updateable"):
if self._price_updateable(new_standard_price):
self.sudo().with_company(company.id).with_context(
disable_auto_svl=True
).standard_price = new_standard_price
else:
if new_standard_price and self.cost_method == "fifo":
self.sudo().with_company(company.id).with_context(
disable_auto_svl=True
).standard_price = new_standard_price
# END HOOK update standard price
# If there's still quantity to value but we're out of candidates,
# we fall in the
# negative stock use case. We chose to value the out move at the price
# of the
# last out and a correction entry will be made once `_fifo_vacuum`
# is called.
vals = {}
if float_is_zero(
qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding
):
vals = {
"value": -tmp_value,
"unit_cost": tmp_value / quantity,
"taken_data": taken_data,
}
else:
assert qty_to_take_on_candidates > 0
last_fifo_price = new_standard_price or self.standard_price
negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
tmp_value += abs(negative_stock_value)
vals = {
"remaining_qty": -qty_to_take_on_candidates,
"value": -tmp_value,
"unit_cost": last_fifo_price,
}
return vals
if not hasattr(ProductProduct, "_run_fifo_original"):
ProductProduct._run_fifo_original = ProductProduct._run_fifo
ProductProduct._run_fifo = _run_fifo_new
def _run_fifo_vacuum_new(self, company=None):
if not hasattr(self, "_run_fifo_prepare_candidate_update"):
return self._run_fifo_vacuum_original(company=company)
if company is None:
company = self.env.company
ValuationLayer = self.env["stock.valuation.layer"].sudo()
svls_to_vacuum_by_product = defaultdict(lambda: ValuationLayer)
res = ValuationLayer.read_group(
[
("product_id", "in", self.ids),
("remaining_qty", "<", 0),
("stock_move_id", "!=", False),
("company_id", "=", company.id),
],
["ids:array_agg(id)", "create_date:min"],
["product_id"],
orderby="create_date, id",
)
min_create_date = datetime.max
for group in res:
svls_to_vacuum_by_product[group["product_id"][0]] = ValuationLayer.browse(
group["ids"]
)
min_create_date = min(min_create_date, group["create_date"])
all_candidates_by_product = defaultdict(lambda: ValuationLayer)
domain = [
("product_id", "in", self.ids),
("remaining_qty", ">", 0),
("company_id", "=", company.id),
("create_date", ">=", min_create_date),
]
if self.env.context.get("use_past_svl", False):
domain = domain[:3]
res = ValuationLayer.sudo().read_group(
domain,
["ids:array_agg(id)"],
["product_id"],
orderby="id",
)
for group in res:
all_candidates_by_product[group["product_id"][0]] = ValuationLayer.browse(
group["ids"]
)
new_svl_vals_real_time = []
new_svl_vals_manual = []
real_time_svls_to_vacuum = ValuationLayer
for product in self:
all_candidates = all_candidates_by_product[product.id]
current_real_time_svls = ValuationLayer
for svl_to_vacuum in svls_to_vacuum_by_product[product.id]:
# We don't use search to avoid executing _flush_search and to decrease interaction with DB
candidates = all_candidates.filtered(
lambda r: r.create_date > svl_to_vacuum.create_date
or r.create_date == svl_to_vacuum.create_date
and r.id > svl_to_vacuum.id
)
if self.env.context.get("use_past_svl", False):
candidates = all_candidates
if not candidates:
break
qty_to_take_on_candidates = abs(svl_to_vacuum.remaining_qty)
qty_taken_on_candidates = 0
tmp_value = 0
taken_data = {}
for candidate in candidates:
qty_taken_on_candidate = min(
candidate.remaining_qty, qty_to_take_on_candidates
)
qty_taken_on_candidates += qty_taken_on_candidate
taken_data[candidate.id] = {"quantity": qty_taken_on_candidate}
candidate_unit_cost = (
candidate.remaining_value / candidate.remaining_qty
)
value_taken_on_candidate = (
qty_taken_on_candidate * candidate_unit_cost
)
value_taken_on_candidate = candidate.currency_id.round(
value_taken_on_candidate
)
taken_data[candidate.id].update(
{
"value": value_taken_on_candidate,
}
)
new_remaining_value = (
candidate.remaining_value - value_taken_on_candidate
)
candidate_vals = {
"remaining_qty": candidate.remaining_qty
- qty_taken_on_candidate,
"remaining_value": new_remaining_value,
}
# Start Hook
candidate_vals = self._run_fifo_vacuum_prepare_candidate_update(
svl_to_vacuum,
candidate,
qty_taken_on_candidate,
value_taken_on_candidate,
candidate_vals,
)
# End Hook
candidate.write(candidate_vals)
if not (candidate.remaining_qty > 0):
all_candidates -= candidate
qty_to_take_on_candidates -= qty_taken_on_candidate
tmp_value += value_taken_on_candidate
if float_is_zero(
qty_to_take_on_candidates,
precision_rounding=product.uom_id.rounding,
):
break
# Get the estimated value we will correct.
remaining_value_before_vacuum = (
svl_to_vacuum.unit_cost * qty_taken_on_candidates
)
new_remaining_qty = (
svl_to_vacuum.remaining_qty + qty_taken_on_candidates
)
corrected_value = remaining_value_before_vacuum - tmp_value
svl_to_vacuum.with_context(taken_data=taken_data).write(
{
"remaining_qty": new_remaining_qty,
}
)
# Don't create a layer or an accounting entry if the corrected value is zero.
if svl_to_vacuum.currency_id.is_zero(corrected_value):
continue
corrected_value = svl_to_vacuum.currency_id.round(corrected_value)
move = svl_to_vacuum.stock_move_id
new_svl_vals = (
new_svl_vals_real_time
if product.valuation == "real_time"
else new_svl_vals_manual
)
new_svl_vals.append(
{
"product_id": product.id,
"value": corrected_value,
"unit_cost": 0,
"quantity": 0,
"remaining_qty": 0,
"stock_move_id": move.id,
"company_id": move.company_id.id,
"description": "Revaluation of %s (negative inventory)"
% (move.picking_id.name or move.name),
"stock_valuation_layer_id": svl_to_vacuum.id,
}
)
if product.valuation == "real_time":
current_real_time_svls |= svl_to_vacuum
real_time_svls_to_vacuum |= current_real_time_svls
ValuationLayer.create(new_svl_vals_manual)
vacuum_svls = ValuationLayer.create(new_svl_vals_real_time)
# If some negative stock were fixed, we need to recompute the standard price.
for product in self:
product = product.with_company(company.id)
# Only recompute if we fixed some negative stock
if not svls_to_vacuum_by_product[product.id]:
continue
if product.cost_method == "average" and not float_is_zero(
product.quantity_svl, precision_rounding=product.uom_id.rounding
):
product.sudo().with_context(disable_auto_svl=True).write(
{"standard_price": product.value_svl / product.quantity_svl}
)
vacuum_svls._validate_accounting_entries()
self._create_fifo_vacuum_anglo_saxon_expense_entries(
zip(vacuum_svls, real_time_svls_to_vacuum)
)
if not hasattr(ProductProduct, "_run_fifo_vacuum_original"):
ProductProduct._run_fifo_vacuum_original = ProductProduct._run_fifo_vacuum
ProductProduct._run_fifo_vacuum = _run_fifo_vacuum_new
def _create_out_svl_new(self, forced_quantity=None):
"""Create a `stock.valuation.layer` from `self`.
:param forced_quantity: under some circunstances, the quantity to value is different than
the initial demand of the move (Default value = None)
"""
svl_vals_list = []
for move in self:
move = move.with_company(move.company_id)
valued_move_lines = move._get_out_move_lines()
valued_quantity = 0
for valued_move_line in valued_move_lines:
valued_quantity += valued_move_line.product_uom_id._compute_quantity(
valued_move_line.qty_done, move.product_id.uom_id
)
if float_is_zero(
forced_quantity or valued_quantity,
precision_rounding=move.product_id.uom_id.rounding,
):
continue
svl_vals = move.product_id.with_context(
move_requestor=move.id
)._prepare_out_svl_vals(forced_quantity or valued_quantity, move.company_id)
svl_vals.update(move._prepare_common_svl_vals())
if forced_quantity:
svl_vals["description"] = (
_("Correction of %s (modification of past move)")
% move.picking_id.name
or move.name
)
svl_vals["description"] += svl_vals.pop("rounding_adjustment", "")
svl_vals_list.append(svl_vals)
return self.env["stock.valuation.layer"].sudo().create(svl_vals_list)
if not hasattr(StockMove, "_create_out_svl_original"):
StockMove._create_out_svl_original = StockMove._create_out_svl
StockMove._create_out_svl = _create_out_svl_new