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.
Files changed (24) hide show
  1. odoo/addons/mrp_multi_level_estimate/README.rst +98 -0
  2. odoo/addons/mrp_multi_level_estimate/__init__.py +2 -0
  3. odoo/addons/mrp_multi_level_estimate/__manifest__.py +19 -0
  4. odoo/addons/mrp_multi_level_estimate/i18n/es.po +117 -0
  5. odoo/addons/mrp_multi_level_estimate/i18n/it.po +137 -0
  6. odoo/addons/mrp_multi_level_estimate/i18n/mrp_multi_level_estimate.pot +83 -0
  7. odoo/addons/mrp_multi_level_estimate/models/__init__.py +2 -0
  8. odoo/addons/mrp_multi_level_estimate/models/mrp_area.py +38 -0
  9. odoo/addons/mrp_multi_level_estimate/models/product_mrp_area.py +26 -0
  10. odoo/addons/mrp_multi_level_estimate/readme/CONFIGURE.rst +5 -0
  11. odoo/addons/mrp_multi_level_estimate/readme/CONTRIBUTORS.rst +2 -0
  12. odoo/addons/mrp_multi_level_estimate/readme/DESCRIPTION.rst +1 -0
  13. odoo/addons/mrp_multi_level_estimate/static/description/icon.png +0 -0
  14. odoo/addons/mrp_multi_level_estimate/static/description/index.html +441 -0
  15. odoo/addons/mrp_multi_level_estimate/tests/__init__.py +1 -0
  16. odoo/addons/mrp_multi_level_estimate/tests/test_mrp_multi_level_estimate.py +463 -0
  17. odoo/addons/mrp_multi_level_estimate/views/mrp_area_views.xml +13 -0
  18. odoo/addons/mrp_multi_level_estimate/views/product_mrp_area_views.xml +14 -0
  19. odoo/addons/mrp_multi_level_estimate/wizards/__init__.py +1 -0
  20. odoo/addons/mrp_multi_level_estimate/wizards/mrp_multi_level.py +138 -0
  21. odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/METADATA +116 -0
  22. odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/RECORD +24 -0
  23. odoo_addon_mrp_multi_level_estimate-16.0.1.2.0.2.dist-info/WHEEL +5 -0
  24. 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