odoo-addon-mrp-multi-level-estimate 16.0.1.2.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/mrp_multi_level_estimate/README.rst +98 -0
- odoo/addons/mrp_multi_level_estimate/__init__.py +2 -0
- odoo/addons/mrp_multi_level_estimate/__manifest__.py +19 -0
- odoo/addons/mrp_multi_level_estimate/i18n/es.po +117 -0
- odoo/addons/mrp_multi_level_estimate/i18n/it.po +137 -0
- odoo/addons/mrp_multi_level_estimate/i18n/mrp_multi_level_estimate.pot +83 -0
- odoo/addons/mrp_multi_level_estimate/models/__init__.py +2 -0
- odoo/addons/mrp_multi_level_estimate/models/mrp_area.py +38 -0
- odoo/addons/mrp_multi_level_estimate/models/product_mrp_area.py +26 -0
- odoo/addons/mrp_multi_level_estimate/readme/CONFIGURE.rst +5 -0
- odoo/addons/mrp_multi_level_estimate/readme/CONTRIBUTORS.rst +2 -0
- odoo/addons/mrp_multi_level_estimate/readme/DESCRIPTION.rst +1 -0
- odoo/addons/mrp_multi_level_estimate/static/description/icon.png +0 -0
- odoo/addons/mrp_multi_level_estimate/static/description/index.html +441 -0
- odoo/addons/mrp_multi_level_estimate/tests/__init__.py +1 -0
- odoo/addons/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py +463 -0
- odoo/addons/mrp_multi_level_estimate/views/mrp_area_views.xml +13 -0
- odoo/addons/mrp_multi_level_estimate/views/product_mrp_area_views.xml +14 -0
- odoo/addons/mrp_multi_level_estimate/wizards/__init__.py +1 -0
- odoo/addons/mrp_multi_level_estimate/wizards/mrp_multi_level.py +138 -0
- odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/METADATA +116 -0
- odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/RECORD +24 -0
- odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/WHEEL +5 -0
- odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Copyright 2018-22 ForgeFlow S.L. (http://www.forgeflow.com)
|
|
2
|
+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
|
3
|
+
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from odoo.tests import Form
|
|
7
|
+
|
|
8
|
+
from odoo.addons.mrp_multi_level.tests.common import TestMrpMultiLevelCommon
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestMrpMultiLevelEstimate(TestMrpMultiLevelCommon):
|
|
12
|
+
@classmethod
|
|
13
|
+
def setUpClass(cls):
|
|
14
|
+
super().setUpClass()
|
|
15
|
+
cls.estimate_obj = cls.env["stock.demand.estimate"]
|
|
16
|
+
|
|
17
|
+
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
|
|
18
|
+
|
|
19
|
+
# Create new clean area:
|
|
20
|
+
cls.estimate_loc = cls.loc_obj.create(
|
|
21
|
+
{
|
|
22
|
+
"name": "Test location for estimates",
|
|
23
|
+
"usage": "internal",
|
|
24
|
+
"location_id": cls.wh.view_location_id.id,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
cls.estimate_area = cls.mrp_area_obj.create(
|
|
28
|
+
{
|
|
29
|
+
"name": "Test",
|
|
30
|
+
"warehouse_id": cls.wh.id,
|
|
31
|
+
"location_id": cls.estimate_loc.id,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
cls.test_mrp_parameter = cls.product_mrp_area_obj.create(
|
|
35
|
+
{
|
|
36
|
+
"product_id": cls.prod_test.id,
|
|
37
|
+
"mrp_area_id": cls.estimate_area.id,
|
|
38
|
+
"mrp_nbr_days": 7,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Create 3 consecutive estimates of 1 week length each.
|
|
43
|
+
today = datetime.today().replace(hour=0)
|
|
44
|
+
date_start_1 = today - timedelta(days=3)
|
|
45
|
+
date_end_1 = date_start_1 + timedelta(days=6)
|
|
46
|
+
date_start_2 = date_end_1 + timedelta(days=1)
|
|
47
|
+
date_end_2 = date_start_2 + timedelta(days=6)
|
|
48
|
+
date_start_3 = date_end_2 + timedelta(days=1)
|
|
49
|
+
date_end_3 = date_start_3 + timedelta(days=6)
|
|
50
|
+
start_dates = [date_start_1, date_start_2, date_start_3]
|
|
51
|
+
end_dates = [date_end_1, date_end_2, date_end_3]
|
|
52
|
+
|
|
53
|
+
cls.date_within_ranges = today - timedelta(days=2)
|
|
54
|
+
cls.date_without_ranges = today + timedelta(days=150)
|
|
55
|
+
|
|
56
|
+
qty = 140.0
|
|
57
|
+
for sd, ed in zip(start_dates, end_dates):
|
|
58
|
+
qty += 70.0
|
|
59
|
+
cls._create_demand_estimate(cls.prod_test, cls.stock_location, sd, ed, qty)
|
|
60
|
+
cls._create_demand_estimate(cls.prod_test, cls.estimate_loc, sd, ed, qty)
|
|
61
|
+
|
|
62
|
+
cls.mrp_multi_level_wiz.create({}).run_mrp_multi_level()
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _create_demand_estimate(cls, product, location, date_from, date_to, qty):
|
|
66
|
+
cls.estimate_obj.create(
|
|
67
|
+
{
|
|
68
|
+
"product_id": product.id,
|
|
69
|
+
"location_id": location.id,
|
|
70
|
+
"product_uom": product.uom_id.id,
|
|
71
|
+
"product_uom_qty": qty,
|
|
72
|
+
"manual_date_from": date_from,
|
|
73
|
+
"manual_date_to": date_to,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def test_01_demand_estimates(self):
|
|
78
|
+
"""Tests demand estimates integration."""
|
|
79
|
+
estimates = self.estimate_obj.search(
|
|
80
|
+
[
|
|
81
|
+
("product_id", "=", self.prod_test.id),
|
|
82
|
+
("location_id", "=", self.stock_location.id),
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(len(estimates), 3)
|
|
86
|
+
moves = self.mrp_move_obj.search(
|
|
87
|
+
[
|
|
88
|
+
("product_id", "=", self.prod_test.id),
|
|
89
|
+
("mrp_area_id", "=", self.mrp_area.id),
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
# 3 weeks - 3 days in the past = 18 days of valid estimates:
|
|
93
|
+
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d")
|
|
94
|
+
self.assertEqual(len(moves_from_estimates), 18)
|
|
95
|
+
quantities = moves_from_estimates.mapped("mrp_qty")
|
|
96
|
+
self.assertIn(-30.0, quantities) # 210 a week => 30.0 dayly:
|
|
97
|
+
self.assertIn(-40.0, quantities) # 280 a week => 40.0 dayly:
|
|
98
|
+
self.assertIn(-50.0, quantities) # 350 a week => 50.0 dayly:
|
|
99
|
+
plans = self.planned_order_obj.search(
|
|
100
|
+
[
|
|
101
|
+
("product_id", "=", self.prod_test.id),
|
|
102
|
+
("mrp_area_id", "=", self.mrp_area.id),
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
action = list(set(plans.mapped("mrp_action")))
|
|
106
|
+
self.assertEqual(len(action), 1)
|
|
107
|
+
self.assertEqual(action[0], "buy")
|
|
108
|
+
self.assertEqual(len(plans), 18)
|
|
109
|
+
inventories = self.mrp_inventory_obj.search(
|
|
110
|
+
[("mrp_area_id", "=", self.estimate_area.id)]
|
|
111
|
+
)
|
|
112
|
+
self.assertEqual(len(inventories), 18)
|
|
113
|
+
|
|
114
|
+
def test_02_demand_estimates_group_plans(self):
|
|
115
|
+
"""Test requirement grouping functionality, `nbr_days`."""
|
|
116
|
+
estimates = self.estimate_obj.search(
|
|
117
|
+
[
|
|
118
|
+
("product_id", "=", self.prod_test.id),
|
|
119
|
+
("location_id", "=", self.estimate_loc.id),
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
self.assertEqual(len(estimates), 3)
|
|
123
|
+
moves = self.mrp_move_obj.search(
|
|
124
|
+
[
|
|
125
|
+
("product_id", "=", self.prod_test.id),
|
|
126
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
supply_plans = self.planned_order_obj.search(
|
|
130
|
+
[
|
|
131
|
+
("product_id", "=", self.prod_test.id),
|
|
132
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
# 3 weeks - 3 days in the past = 18 days of valid estimates:
|
|
136
|
+
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d")
|
|
137
|
+
self.assertEqual(len(moves_from_estimates), 18)
|
|
138
|
+
# 18 days of demand / 7 nbr_days = 2.57 => 3 supply moves expected.
|
|
139
|
+
self.assertEqual(len(supply_plans), 3)
|
|
140
|
+
quantities = supply_plans.mapped("mrp_qty")
|
|
141
|
+
week_1_expected = sum(moves_from_estimates[0:7].mapped("mrp_qty"))
|
|
142
|
+
self.assertIn(abs(week_1_expected), quantities)
|
|
143
|
+
week_2_expected = sum(moves_from_estimates[7:14].mapped("mrp_qty"))
|
|
144
|
+
self.assertIn(abs(week_2_expected), quantities)
|
|
145
|
+
week_3_expected = sum(moves_from_estimates[14:].mapped("mrp_qty"))
|
|
146
|
+
self.assertIn(abs(week_3_expected), quantities)
|
|
147
|
+
|
|
148
|
+
def test_03_group_demand_estimates(self):
|
|
149
|
+
"""Test demand grouping functionality, `group_estimate_days`."""
|
|
150
|
+
self.test_mrp_parameter.group_estimate_days = 7
|
|
151
|
+
self.mrp_multi_level_wiz.create(
|
|
152
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
153
|
+
).run_mrp_multi_level()
|
|
154
|
+
estimates = self.estimate_obj.search(
|
|
155
|
+
[
|
|
156
|
+
("product_id", "=", self.prod_test.id),
|
|
157
|
+
("location_id", "=", self.estimate_loc.id),
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
self.assertEqual(len(estimates), 3)
|
|
161
|
+
moves = self.mrp_move_obj.search(
|
|
162
|
+
[
|
|
163
|
+
("product_id", "=", self.prod_test.id),
|
|
164
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
165
|
+
]
|
|
166
|
+
)
|
|
167
|
+
# 3 weekly estimates, demand from estimates grouped in batches of 7
|
|
168
|
+
# days = 3 days of estimates mrp moves:
|
|
169
|
+
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d")
|
|
170
|
+
self.assertEqual(len(moves_from_estimates), 3)
|
|
171
|
+
# 210 weekly -> 30 daily -> 30 * 4 days not consumed = 120
|
|
172
|
+
self.assertEqual(moves_from_estimates[0].mrp_qty, -120)
|
|
173
|
+
self.assertEqual(moves_from_estimates[1].mrp_qty, -280)
|
|
174
|
+
self.assertEqual(moves_from_estimates[2].mrp_qty, -350)
|
|
175
|
+
# Test group_estimate_days greater than date range, it should not
|
|
176
|
+
# generate greater demand.
|
|
177
|
+
self.test_mrp_parameter.group_estimate_days = 10
|
|
178
|
+
self.mrp_multi_level_wiz.create(
|
|
179
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
180
|
+
).run_mrp_multi_level()
|
|
181
|
+
moves = self.mrp_move_obj.search(
|
|
182
|
+
[
|
|
183
|
+
("product_id", "=", self.prod_test.id),
|
|
184
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d")
|
|
188
|
+
self.assertEqual(len(moves_from_estimates), 3)
|
|
189
|
+
self.assertEqual(moves_from_estimates[0].mrp_qty, -120)
|
|
190
|
+
self.assertEqual(moves_from_estimates[1].mrp_qty, -280)
|
|
191
|
+
self.assertEqual(moves_from_estimates[2].mrp_qty, -350)
|
|
192
|
+
# Test group_estimate_days smaller than date range, it should not
|
|
193
|
+
# generate greater demand.
|
|
194
|
+
self.test_mrp_parameter.group_estimate_days = 5
|
|
195
|
+
self.mrp_multi_level_wiz.create(
|
|
196
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
197
|
+
).run_mrp_multi_level()
|
|
198
|
+
moves = self.mrp_move_obj.search(
|
|
199
|
+
[
|
|
200
|
+
("product_id", "=", self.prod_test.id),
|
|
201
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
202
|
+
]
|
|
203
|
+
)
|
|
204
|
+
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d")
|
|
205
|
+
self.assertEqual(len(moves_from_estimates), 5)
|
|
206
|
+
# Week 1 partially consumed, so only 4 remaining days considered.
|
|
207
|
+
# 30 daily x 4 days = 120
|
|
208
|
+
self.assertEqual(moves_from_estimates[0].mrp_qty, -120)
|
|
209
|
+
# Week 2 divided in 2 (40 daily) -> 5 days = 200, 2 days = 80
|
|
210
|
+
self.assertEqual(moves_from_estimates[1].mrp_qty, -200)
|
|
211
|
+
self.assertEqual(moves_from_estimates[2].mrp_qty, -80)
|
|
212
|
+
# Week 3 divided in 2, (50 daily) -> 5 days = 250, 2 days = 100
|
|
213
|
+
self.assertEqual(moves_from_estimates[3].mrp_qty, -250)
|
|
214
|
+
self.assertEqual(moves_from_estimates[4].mrp_qty, -100)
|
|
215
|
+
|
|
216
|
+
def test_04_group_demand_estimates_rounding(self):
|
|
217
|
+
"""Test demand grouping functionality, `group_estimate_days` and rounding."""
|
|
218
|
+
self.test_mrp_parameter.group_estimate_days = 7
|
|
219
|
+
self.uom_unit.rounding = 1.00
|
|
220
|
+
|
|
221
|
+
estimates = self.estimate_obj.search(
|
|
222
|
+
[
|
|
223
|
+
("product_id", "=", self.prod_test.id),
|
|
224
|
+
("location_id", "=", self.estimate_loc.id),
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
self.assertEqual(len(estimates), 3)
|
|
228
|
+
# Change qty of estimates to quantities that divided by 7 days return a decimal result
|
|
229
|
+
qty = 400
|
|
230
|
+
for estimate in estimates:
|
|
231
|
+
estimate.product_uom_qty = qty
|
|
232
|
+
qty += 100
|
|
233
|
+
|
|
234
|
+
self.mrp_multi_level_wiz.create(
|
|
235
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
236
|
+
).run_mrp_multi_level()
|
|
237
|
+
moves = self.mrp_move_obj.search(
|
|
238
|
+
[
|
|
239
|
+
("product_id", "=", self.prod_test.id),
|
|
240
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
241
|
+
]
|
|
242
|
+
)
|
|
243
|
+
# 3 weekly estimates, demand from estimates grouped in batches of 7
|
|
244
|
+
# days = 3 days of estimates mrp moves:
|
|
245
|
+
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == "d")
|
|
246
|
+
self.assertEqual(len(moves_from_estimates), 3)
|
|
247
|
+
# Rounding should be done at the end of the calculation, using the daily
|
|
248
|
+
# quantity already rounded can lead to errors.
|
|
249
|
+
# 400 weekly -> 57.41 daily -> 57.41 * 4 days not consumed = 228,57 = 229
|
|
250
|
+
self.assertEqual(moves_from_estimates[0].mrp_qty, -229)
|
|
251
|
+
# 500 weekly -> 71.42 daily -> 71,42 * 7 = 500
|
|
252
|
+
self.assertEqual(moves_from_estimates[1].mrp_qty, -500)
|
|
253
|
+
# 600 weekly -> 85.71 daily -> 85.71 * 7 = 600
|
|
254
|
+
self.assertEqual(moves_from_estimates[2].mrp_qty, -600)
|
|
255
|
+
|
|
256
|
+
def test_05_estimate_and_other_sources_strat(self):
|
|
257
|
+
"""Tests demand estimates and other sources strategies."""
|
|
258
|
+
estimates = self.estimate_obj.search(
|
|
259
|
+
[
|
|
260
|
+
("product_id", "=", self.prod_test.id),
|
|
261
|
+
("location_id", "=", self.estimate_loc.id),
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
self.assertEqual(len(estimates), 3)
|
|
265
|
+
self._create_picking_out(
|
|
266
|
+
self.prod_test, 25, self.date_within_ranges, location=self.estimate_loc
|
|
267
|
+
)
|
|
268
|
+
self._create_picking_out(
|
|
269
|
+
self.prod_test, 25, self.date_without_ranges, location=self.estimate_loc
|
|
270
|
+
)
|
|
271
|
+
# 1. "all"
|
|
272
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = "all"
|
|
273
|
+
self.mrp_multi_level_wiz.create(
|
|
274
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
275
|
+
).run_mrp_multi_level()
|
|
276
|
+
moves = self.mrp_move_obj.search(
|
|
277
|
+
[
|
|
278
|
+
("product_id", "=", self.prod_test.id),
|
|
279
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
280
|
+
]
|
|
281
|
+
)
|
|
282
|
+
# 3 weeks - 3 days in the past = 18 days of valid estimates:
|
|
283
|
+
demand_from_estimates = moves.filtered(
|
|
284
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
285
|
+
)
|
|
286
|
+
demand_from_other_sources = moves.filtered(
|
|
287
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
288
|
+
)
|
|
289
|
+
self.assertEqual(len(demand_from_estimates), 18)
|
|
290
|
+
self.assertEqual(len(demand_from_other_sources), 2)
|
|
291
|
+
|
|
292
|
+
# 2. "ignore_others_if_estimates"
|
|
293
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = (
|
|
294
|
+
"ignore_others_if_estimates"
|
|
295
|
+
)
|
|
296
|
+
self.mrp_multi_level_wiz.create(
|
|
297
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
298
|
+
).run_mrp_multi_level()
|
|
299
|
+
moves = self.mrp_move_obj.search(
|
|
300
|
+
[
|
|
301
|
+
("product_id", "=", self.prod_test.id),
|
|
302
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
303
|
+
]
|
|
304
|
+
)
|
|
305
|
+
demand_from_estimates = moves.filtered(
|
|
306
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
307
|
+
)
|
|
308
|
+
demand_from_other_sources = moves.filtered(
|
|
309
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
310
|
+
)
|
|
311
|
+
self.assertEqual(len(demand_from_estimates), 18)
|
|
312
|
+
self.assertEqual(len(demand_from_other_sources), 0)
|
|
313
|
+
|
|
314
|
+
# 3. "ignore_overlapping"
|
|
315
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = (
|
|
316
|
+
"ignore_overlapping"
|
|
317
|
+
)
|
|
318
|
+
self.mrp_multi_level_wiz.create(
|
|
319
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
320
|
+
).run_mrp_multi_level()
|
|
321
|
+
moves = self.mrp_move_obj.search(
|
|
322
|
+
[
|
|
323
|
+
("product_id", "=", self.prod_test.id),
|
|
324
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
demand_from_estimates = moves.filtered(
|
|
328
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
329
|
+
)
|
|
330
|
+
demand_from_other_sources = moves.filtered(
|
|
331
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
332
|
+
)
|
|
333
|
+
self.assertEqual(len(demand_from_estimates), 18)
|
|
334
|
+
self.assertEqual(len(demand_from_other_sources), 1)
|
|
335
|
+
self.assertEqual(
|
|
336
|
+
demand_from_other_sources.mrp_date, self.date_without_ranges.date()
|
|
337
|
+
)
|
|
338
|
+
# 4. "ignore_estimates"
|
|
339
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = "ignore_estimates"
|
|
340
|
+
self.mrp_multi_level_wiz.create(
|
|
341
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
342
|
+
).run_mrp_multi_level()
|
|
343
|
+
moves = self.mrp_move_obj.search(
|
|
344
|
+
[
|
|
345
|
+
("product_id", "=", self.prod_test.id),
|
|
346
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
347
|
+
]
|
|
348
|
+
)
|
|
349
|
+
demand_from_estimates = moves.filtered(
|
|
350
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
351
|
+
)
|
|
352
|
+
demand_from_other_sources = moves.filtered(
|
|
353
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
354
|
+
)
|
|
355
|
+
self.assertEqual(len(demand_from_estimates), 0)
|
|
356
|
+
self.assertEqual(len(demand_from_other_sources), 2)
|
|
357
|
+
|
|
358
|
+
def test_06_estimate_and_other_sources_strat_with_mo(self):
|
|
359
|
+
"""
|
|
360
|
+
Tests demand estimates and other sources strategies with MOs.
|
|
361
|
+
Components demand from MOs is always indirect demand, so even if we
|
|
362
|
+
have estimates, we should consider that demand.
|
|
363
|
+
"""
|
|
364
|
+
# Get manufactured product, component and bom
|
|
365
|
+
fp_1 = self.env.ref("mrp_multi_level.product_product_fp_1")
|
|
366
|
+
pp_1 = self.env.ref("mrp_multi_level.product_product_pp_1")
|
|
367
|
+
fp_1_bom = self.env.ref("mrp_multi_level.mrp_bom_fp_1")
|
|
368
|
+
self.product_mrp_area_obj.create(
|
|
369
|
+
{"product_id": fp_1.id, "mrp_area_id": self.estimate_area.id}
|
|
370
|
+
)
|
|
371
|
+
self.product_mrp_area_obj.create(
|
|
372
|
+
{"product_id": pp_1.id, "mrp_area_id": self.estimate_area.id}
|
|
373
|
+
)
|
|
374
|
+
# Create 1 estimate of 1 week length for the component.
|
|
375
|
+
date_start = datetime.today().replace(hour=0)
|
|
376
|
+
date_end = date_start + timedelta(days=6)
|
|
377
|
+
self._create_demand_estimate(pp_1, self.estimate_loc, date_start, date_end, 7)
|
|
378
|
+
date_mo = date_start + timedelta(days=1)
|
|
379
|
+
# Create 1 MO for fp_1 that has two pp_1 in its components
|
|
380
|
+
mo_form = Form(self.mo_obj)
|
|
381
|
+
mo_form.product_id = fp_1
|
|
382
|
+
mo_form.bom_id = fp_1_bom
|
|
383
|
+
mo_form.product_qty = 10
|
|
384
|
+
mo_form.date_planned_start = date_mo
|
|
385
|
+
mo = mo_form.save()
|
|
386
|
+
mo.location_src_id = (
|
|
387
|
+
self.estimate_loc
|
|
388
|
+
) # writing afterward to avoid invisible-field error in Form.
|
|
389
|
+
mo.action_confirm()
|
|
390
|
+
# Create 1 picking out that represents a Delivery Order from a sale
|
|
391
|
+
self._create_picking_out(pp_1, 5, date_mo, location=self.estimate_loc)
|
|
392
|
+
# 1. "all"
|
|
393
|
+
# Expected result: Consider all sources of demand
|
|
394
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = "all"
|
|
395
|
+
self.mrp_multi_level_wiz.create(
|
|
396
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
397
|
+
).run_mrp_multi_level()
|
|
398
|
+
moves = self.mrp_move_obj.search(
|
|
399
|
+
[
|
|
400
|
+
("product_id", "=", pp_1.id),
|
|
401
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
402
|
+
]
|
|
403
|
+
)
|
|
404
|
+
demand_from_estimates = moves.filtered(
|
|
405
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
406
|
+
)
|
|
407
|
+
demand_from_other_sources = moves.filtered(
|
|
408
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
409
|
+
)
|
|
410
|
+
self.assertEqual(len(demand_from_estimates), 7)
|
|
411
|
+
self.assertEqual(sum(demand_from_estimates.mapped("mrp_qty")), -7)
|
|
412
|
+
self.assertEqual(len(demand_from_other_sources), 2)
|
|
413
|
+
self.assertEqual(sum(demand_from_other_sources.mapped("mrp_qty")), -25)
|
|
414
|
+
|
|
415
|
+
# 2. "ignore_others_if_estimates"
|
|
416
|
+
# Expected result: Consider estimates and demand from MO
|
|
417
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = (
|
|
418
|
+
"ignore_others_if_estimates"
|
|
419
|
+
)
|
|
420
|
+
self.mrp_multi_level_wiz.create(
|
|
421
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
422
|
+
).run_mrp_multi_level()
|
|
423
|
+
moves = self.mrp_move_obj.search(
|
|
424
|
+
[
|
|
425
|
+
("product_id", "=", pp_1.id),
|
|
426
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
demand_from_estimates = moves.filtered(
|
|
430
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
431
|
+
)
|
|
432
|
+
demand_from_other_sources = moves.filtered(
|
|
433
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
434
|
+
)
|
|
435
|
+
self.assertEqual(len(demand_from_estimates), 7)
|
|
436
|
+
self.assertEqual(sum(demand_from_estimates.mapped("mrp_qty")), -7)
|
|
437
|
+
self.assertEqual(len(demand_from_other_sources), 1)
|
|
438
|
+
self.assertEqual(sum(demand_from_other_sources.mapped("mrp_qty")), -20)
|
|
439
|
+
|
|
440
|
+
# 3. "ignore_overlapping"
|
|
441
|
+
# Expected result: Consider estimates and demand from MO
|
|
442
|
+
self.estimate_area.estimate_demand_and_other_sources_strat = (
|
|
443
|
+
"ignore_overlapping"
|
|
444
|
+
)
|
|
445
|
+
self.mrp_multi_level_wiz.create(
|
|
446
|
+
{"mrp_area_ids": [(6, 0, self.estimate_area.ids)]}
|
|
447
|
+
).run_mrp_multi_level()
|
|
448
|
+
moves = self.mrp_move_obj.search(
|
|
449
|
+
[
|
|
450
|
+
("product_id", "=", pp_1.id),
|
|
451
|
+
("mrp_area_id", "=", self.estimate_area.id),
|
|
452
|
+
]
|
|
453
|
+
)
|
|
454
|
+
demand_from_estimates = moves.filtered(
|
|
455
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin == "fc"
|
|
456
|
+
)
|
|
457
|
+
demand_from_other_sources = moves.filtered(
|
|
458
|
+
lambda m: m.mrp_type == "d" and m.mrp_origin != "fc"
|
|
459
|
+
)
|
|
460
|
+
self.assertEqual(len(demand_from_estimates), 7)
|
|
461
|
+
self.assertEqual(sum(demand_from_estimates.mapped("mrp_qty")), -7)
|
|
462
|
+
self.assertEqual(len(demand_from_other_sources), 1)
|
|
463
|
+
self.assertEqual(sum(demand_from_other_sources.mapped("mrp_qty")), -20)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<?xml version="1.0" ?>
|
|
2
|
+
<odoo>
|
|
3
|
+
<record id="mrp_area_form" model="ir.ui.view">
|
|
4
|
+
<field name="name">mrp.area.form - mrp_multi_level_estimate</field>
|
|
5
|
+
<field name="model">mrp.area</field>
|
|
6
|
+
<field name="inherit_id" ref="mrp_multi_level.mrp_area_form" />
|
|
7
|
+
<field name="arch" type="xml">
|
|
8
|
+
<group name="settings" position="inside">
|
|
9
|
+
<field name="estimate_demand_and_other_sources_strat" />
|
|
10
|
+
</group>
|
|
11
|
+
</field>
|
|
12
|
+
</record>
|
|
13
|
+
</odoo>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" ?>
|
|
2
|
+
<odoo>
|
|
3
|
+
<record id="product_mrp_area_form" model="ir.ui.view">
|
|
4
|
+
<field name="name">product.mrp.area.form - estimates</field>
|
|
5
|
+
<field name="model">product.mrp.area</field>
|
|
6
|
+
<field name="type">form</field>
|
|
7
|
+
<field name="inherit_id" ref="mrp_multi_level.product_mrp_area_form" />
|
|
8
|
+
<field name="arch" type="xml">
|
|
9
|
+
<field name="mrp_nbr_days" position="after">
|
|
10
|
+
<field name="group_estimate_days" />
|
|
11
|
+
</field>
|
|
12
|
+
</field>
|
|
13
|
+
</record>
|
|
14
|
+
</odoo>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import mrp_multi_level
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright 2019-22 ForgeFlow S.L. (http://www.forgeflow.com)
|
|
2
|
+
# - Lois Rilo <lois.rilo@forgeflow.com>
|
|
3
|
+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
from odoo import api, fields, models
|
|
9
|
+
from odoo.tools.float_utils import float_round
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MultiLevelMrp(models.TransientModel):
|
|
15
|
+
_inherit = "mrp.multi.level"
|
|
16
|
+
|
|
17
|
+
@api.model
|
|
18
|
+
def _prepare_mrp_move_data_from_estimate(self, estimate, product_mrp_area, date):
|
|
19
|
+
mrp_type = "d"
|
|
20
|
+
origin = "fc"
|
|
21
|
+
daily_qty_unrounded = estimate.daily_qty
|
|
22
|
+
daily_qty = float_round(
|
|
23
|
+
estimate.daily_qty,
|
|
24
|
+
precision_rounding=product_mrp_area.product_id.uom_id.rounding,
|
|
25
|
+
rounding_method="HALF-UP",
|
|
26
|
+
)
|
|
27
|
+
days_consumed = 0
|
|
28
|
+
if product_mrp_area.group_estimate_days > 1:
|
|
29
|
+
start = estimate.date_from
|
|
30
|
+
if start < date:
|
|
31
|
+
days_consumed = (date - start).days
|
|
32
|
+
group_estimate_days = min(
|
|
33
|
+
product_mrp_area.group_estimate_days, estimate.duration - days_consumed
|
|
34
|
+
)
|
|
35
|
+
mrp_qty = float_round(
|
|
36
|
+
daily_qty_unrounded * group_estimate_days,
|
|
37
|
+
precision_rounding=product_mrp_area.product_id.uom_id.rounding,
|
|
38
|
+
rounding_method="HALF-UP",
|
|
39
|
+
)
|
|
40
|
+
return {
|
|
41
|
+
"mrp_area_id": product_mrp_area.mrp_area_id.id,
|
|
42
|
+
"product_id": product_mrp_area.product_id.id,
|
|
43
|
+
"product_mrp_area_id": product_mrp_area.id,
|
|
44
|
+
"production_id": None,
|
|
45
|
+
"purchase_order_id": None,
|
|
46
|
+
"purchase_line_id": None,
|
|
47
|
+
"stock_move_id": None,
|
|
48
|
+
"mrp_qty": -mrp_qty,
|
|
49
|
+
"current_qty": -daily_qty,
|
|
50
|
+
"mrp_date": date,
|
|
51
|
+
"current_date": date,
|
|
52
|
+
"mrp_type": mrp_type,
|
|
53
|
+
"mrp_origin": origin,
|
|
54
|
+
"mrp_order_number": None,
|
|
55
|
+
"parent_product_id": None,
|
|
56
|
+
"name": "Forecast",
|
|
57
|
+
"state": "confirmed",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@api.model
|
|
61
|
+
def _estimates_domain(self, product_mrp_area):
|
|
62
|
+
estimate_strat = (
|
|
63
|
+
product_mrp_area.mrp_area_id.estimate_demand_and_other_sources_strat
|
|
64
|
+
)
|
|
65
|
+
if estimate_strat == "ignore_estimates":
|
|
66
|
+
# Return an impossible domain to ignore estimates.
|
|
67
|
+
return [("location_id", "=", 1), ("location_id", "=", 2)]
|
|
68
|
+
locations = product_mrp_area.mrp_area_id._get_locations()
|
|
69
|
+
return [
|
|
70
|
+
("product_id", "=", product_mrp_area.product_id.id),
|
|
71
|
+
("location_id", "child_of", locations.ids),
|
|
72
|
+
("date_to", ">=", fields.Date.today()),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
@api.model
|
|
76
|
+
def _init_mrp_move_from_forecast(self, product_mrp_area):
|
|
77
|
+
res = super(MultiLevelMrp, self)._init_mrp_move_from_forecast(product_mrp_area)
|
|
78
|
+
if not product_mrp_area.group_estimate_days:
|
|
79
|
+
return False
|
|
80
|
+
today = fields.Date.today()
|
|
81
|
+
domain = self._estimates_domain(product_mrp_area)
|
|
82
|
+
estimates = self.env["stock.demand.estimate"].search(domain)
|
|
83
|
+
for rec in estimates:
|
|
84
|
+
start = rec.date_from
|
|
85
|
+
if start < today:
|
|
86
|
+
start = today
|
|
87
|
+
mrp_date = fields.Date.from_string(start)
|
|
88
|
+
date_end = fields.Date.from_string(rec.date_to)
|
|
89
|
+
delta = timedelta(days=product_mrp_area.group_estimate_days)
|
|
90
|
+
while mrp_date <= date_end:
|
|
91
|
+
mrp_move_data = self._prepare_mrp_move_data_from_estimate(
|
|
92
|
+
rec, product_mrp_area, mrp_date
|
|
93
|
+
)
|
|
94
|
+
self.env["mrp.move"].create(mrp_move_data)
|
|
95
|
+
mrp_date += delta
|
|
96
|
+
return res
|
|
97
|
+
|
|
98
|
+
def _exclude_considering_estimate_demand_and_other_sources_strat(
|
|
99
|
+
self, product_mrp_area, mrp_date
|
|
100
|
+
):
|
|
101
|
+
estimate_strat = (
|
|
102
|
+
product_mrp_area.mrp_area_id.estimate_demand_and_other_sources_strat
|
|
103
|
+
)
|
|
104
|
+
if estimate_strat == "all":
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
domain = self._estimates_domain(product_mrp_area)
|
|
108
|
+
estimates = self.env["stock.demand.estimate"].search(domain)
|
|
109
|
+
if not estimates:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
if estimate_strat == "ignore_others_if_estimates":
|
|
113
|
+
# Ignore
|
|
114
|
+
return True
|
|
115
|
+
if estimate_strat == "ignore_overlapping":
|
|
116
|
+
for estimate in estimates:
|
|
117
|
+
if mrp_date >= estimate.date_from and mrp_date <= estimate.date_to:
|
|
118
|
+
# Ignore
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
@api.model
|
|
123
|
+
def _prepare_mrp_move_data_from_stock_move(
|
|
124
|
+
self, product_mrp_area, move, direction="in"
|
|
125
|
+
):
|
|
126
|
+
res = super()._prepare_mrp_move_data_from_stock_move(
|
|
127
|
+
product_mrp_area, move, direction=direction
|
|
128
|
+
)
|
|
129
|
+
if direction == "out":
|
|
130
|
+
mrp_date = res.get("mrp_date")
|
|
131
|
+
if (
|
|
132
|
+
self._exclude_considering_estimate_demand_and_other_sources_strat(
|
|
133
|
+
product_mrp_area, mrp_date
|
|
134
|
+
)
|
|
135
|
+
and not res["production_id"]
|
|
136
|
+
):
|
|
137
|
+
return False
|
|
138
|
+
return res
|