This repository has been archived by the owner on Dec 30, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
production.py
246 lines (217 loc) · 9.06 KB
/
production.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
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from collections import defaultdict
from trytond.model import ModelSQL, ValueMixin, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import TimeDelta
from trytond.tools import grouped_slice
from trytond.transaction import Transaction
supply_period = fields.TimeDelta(
"Supply Period",
domain=['OR',
('supply_period', '=', None),
('supply_period', '>=', TimeDelta()),
])
class Configuration(metaclass=PoolMeta):
__name__ = 'production.configuration'
supply_period = fields.MultiValue(supply_period)
class ConfigurationSupplyPeriod(ModelSQL, ValueMixin):
"Production Configuration Supply Period"
__name__ = 'production.configuration.supply_period'
supply_period = supply_period
class Production(metaclass=PoolMeta):
__name__ = 'production'
@classmethod
def _get_origin(cls):
origins = super()._get_origin()
return origins | {'stock.order_point'}
@classmethod
def generate_requests(cls, clean=True, warehouses=None):
"""
For each product compute the production request that must be created
today to meet product outputs.
If clean is set, it will remove all previous requests.
If warehouses is specified it will compute the production requests
only for the selected warehouses.
"""
pool = Pool()
OrderPoint = pool.get('stock.order_point')
Product = pool.get('product.product')
Location = pool.get('stock.location')
Date = pool.get('ir.date')
User = pool.get('res.user')
company = User(Transaction().user).company
if clean:
reqs = cls.search([
('state', '=', 'request'),
('origin', 'like', 'stock.order_point,%'),
])
if warehouses:
reqs = [r for r in reqs if r.warehouse in warehouses]
cls.delete(reqs)
if warehouses is None:
# fetch warehouse
warehouses = Location.search([
('type', '=', 'warehouse'),
])
warehouse_ids = [w.id for w in warehouses]
# fetch order points
order_points = OrderPoint.search([
('warehouse_location', '!=', None),
('company', '=', company.id if company else None),
])
# index them by product
product2ops = {}
product2ops_other = {}
for order_point in order_points:
if order_point.type == 'production':
dict_ = product2ops
else:
dict_ = product2ops_other
dict_[
(order_point.warehouse_location.id, order_point.product.id)
] = order_point
# fetch goods
products = Product.search([
('type', '=', 'goods'),
('consumable', '=', False),
('producible', '=', True),
])
# compute requests
today = Date.today()
# aggregate product by supply period
date2products = defaultdict(list)
for product in products:
min_date = today
max_date = today + product.get_supply_period()
date2products[min_date, max_date].append(product)
requests = []
for (min_date, max_date), dates_products in date2products.items():
for sub_products in grouped_slice(products):
sub_products = list(sub_products)
product_ids = [p.id for p in sub_products]
with Transaction().set_context(
forecast=True,
stock_date_end=min_date):
pbl = Product.products_by_location(
warehouse_ids,
with_childs=True,
grouping_filter=(product_ids,))
for warehouse in warehouses:
min_date_qties = defaultdict(int,
((x, pbl.pop((warehouse.id, x), 0))
for x in product_ids))
# Do not compute shortage for product
# with different order point
product_ids = [
p.id for p in sub_products
if (warehouse.id, p.id) not in product2ops_other]
# Search for shortage between min-max
shortages = cls.get_shortage(
warehouse.id, product_ids, min_date, max_date,
min_date_qties=min_date_qties,
order_points=product2ops)
for product in sub_products:
if product.id not in shortages:
continue
for date, quantity in shortages[product.id]:
order_point = product2ops.get(
(warehouse.id, product.id))
req = cls.compute_request(product, warehouse,
quantity, date, company, order_point)
req.planned_start_date = (
req.on_change_with_planned_start_date())
requests.append(req)
cls.save(requests)
cls.set_moves(requests)
return requests
@classmethod
def compute_request(
cls, product, warehouse, quantity, date, company,
order_point=None):
"""
Return the value of the production request.
"""
pool = Pool()
Date = pool.get('ir.date')
with Transaction().set_context(company=company.id):
today = Date.today()
if date <= today:
date = today
else:
date -= datetime.timedelta(1)
uom = product.default_uom
quantity = uom.ceil(quantity)
if order_point:
origin = str(order_point)
else:
origin = 'stock.order_point,-1'
return cls(
planned_date=date,
company=company,
warehouse=warehouse,
location=warehouse.production_location,
product=product,
bom=product.boms[0].bom if product.boms else None,
uom=uom,
quantity=quantity,
state='request',
origin=origin,
)
@classmethod
def get_shortage(cls, location_id, product_ids, min_date, max_date,
min_date_qties, order_points):
"""
Return for each product a list of dates where the stock quantity is
less than the minimal quantity and the quantity to reach the maximal
quantity over the period.
The minimal and maximal quantities come from the order point or are
zero.
min_date_qty is the quantities for each product at the min date.
order_points is a dictionary that links products to order points.
"""
pool = Pool()
Product = pool.get('product.product')
shortages = defaultdict(list)
min_quantities = defaultdict(float)
target_quantities = defaultdict(float)
for product_id in product_ids:
order_point = order_points.get((location_id, product_id))
if order_point:
min_quantities[product_id] = order_point.min_quantity
target_quantities[product_id] = order_point.target_quantity
with Transaction().set_context(
forecast=True,
stock_date_start=min_date,
stock_date_end=max_date):
pbl = Product.products_by_location(
[location_id],
with_childs=True,
grouping=('date', 'product'),
grouping_filter=(None, product_ids))
pbl_dates = defaultdict(dict)
for key, qty in pbl.items():
date, product_id = key[1:]
pbl_dates[date][product_id] = qty
current_date = min_date
current_qties = min_date_qties.copy()
products_to_check = product_ids.copy()
while (current_date < max_date) or (current_date == min_date):
for product_id in products_to_check:
current_qty = current_qties[product_id]
min_quantity = min_quantities[product_id]
if min_quantity is not None and current_qty < min_quantity:
target_quantity = target_quantities[product_id]
quantity = target_quantity - current_qty
shortages[product_id].append((current_date, quantity))
current_qties[product_id] += quantity
if current_date == datetime.date.max:
break
current_date += datetime.timedelta(1)
pbl = pbl_dates[current_date]
products_to_check.clear()
for product_id, qty in pbl.items():
current_qties[product_id] += qty
products_to_check.append(product_id)
return shortages