2
# -*- encoding: utf-8 -*-
3
##############################################################################
5
# OpenERP, Open Source Management Solution
6
# Copyright (C) 2011 TeMPO Consulting, MSF
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Affero General Public License as
10
# published by the Free Software Foundation, either version 3 of the
11
# License, or (at your option) any later version.
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU Affero General Public License for more details.
18
# You should have received a copy of the GNU Affero General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
##############################################################################
24
from osv import fields
26
from mx.DateTime import DateFrom
27
from mx.DateTime import now
28
from mx.DateTime import RelativeDate
30
class threshold_value(osv.osv):
31
_name = 'threshold.value'
32
_description = 'Threshold value'
34
def _get_product_ids(self, cr, uid, ids, field_name, arg, context=None):
36
Returns a list of products for the rule
40
for rule in self.browse(cr, uid, ids, context=context):
42
for line in rule.line_ids:
43
res[rule.id].append(line.product_id.id)
47
def _src_product_ids(self, cr, uid, obj, name, args, context=None):
54
if arg[0] == 'product_ids':
56
line_ids = self.pool.get('threshold.value.line').search(cr, uid, [('product_id', arg[1], arg[2])])
57
for l in self.pool.get('threshold.value.line').browse(cr, uid, line_ids):
58
if l.threshold_value_id.id not in rule_ids:
59
rule_ids.append(l.threshold_value_id.id)
60
res.append(('id', 'in', rule_ids))
65
'name': fields.char(size=128, string='Reference', required=True),
66
'active': fields.boolean(string='Active'),
67
'warehouse_id': fields.many2one('stock.warehouse', string='Warehouse', required=True),
68
'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade",
69
domain="[('is_replenishment', '=', warehouse_id)]",
70
help='Location where the computation is made'),
71
'compute_method': fields.selection([('fixed', 'Fixed values'), ('computed', 'Computed values')],
72
string='Method of computation', required=True,
73
help="""If 'Fixed values', the scheduler will compare stock of product with the threshold value of the line. \n
74
If 'Computed values', the threshold value and the ordered quantity will be calculated according to defined parameters"""),
75
'consumption_method': fields.selection([('amc', 'Average Monthly Consumption'), ('fmc', 'Forecasted Monthly Consumption')],
76
string='Consumption Method',
77
help='Method used to compute the consumption of products.'),
78
'consumption_period_from': fields.date(string='Period of calculation',
79
help='This period is a number of past months the system has to consider for AMC calculation.'\
80
'By default this value is equal to the frequency in the Threshold.'),
81
'consumption_period_to': fields.date(string='-'),
82
'frequency': fields.float(digits=(16,2), string='Order frequency',
83
help='The time between two replenishments. Will be used to compute the quantity to order.'),
84
'safety_month': fields.float(digits=(16,2), string='Safety Stock in months',
85
help='In months. Period during the stock is not empty but need to be replenish. \
86
Used to compute the quantity to order.'),
87
'lead_time': fields.float(digits=(16,2), string='Fixed Lead Time in months',
88
help='In months. Time to be delivered after processing the purchase order.'),
89
'supplier_lt': fields.boolean(string='Product\'s supplier LT',
90
help='If checked, use the lead time set in the supplier form.'),
91
'line_ids': fields.one2many('threshold.value.line', 'threshold_value_id', string="Products"),
92
'fixed_line_ids': fields.one2many('threshold.value.line', 'threshold_value_id2', string="Products"),
93
'product_ids': fields.function(_get_product_ids, fnct_search=_src_product_ids,
94
type='many2many', relation='product.product', method=True, string='Products'),
95
'sublist_id': fields.many2one('product.list', string='List/Sublist'),
96
'nomen_manda_0': fields.many2one('product.nomenclature', 'Main Type'),
97
'nomen_manda_1': fields.many2one('product.nomenclature', 'Group'),
98
'nomen_manda_2': fields.many2one('product.nomenclature', 'Family'),
99
'nomen_manda_3': fields.many2one('product.nomenclature', 'Root'),
103
'name': lambda obj, cr, uid, context=None: obj.pool.get('ir.sequence').get(cr, uid, 'threshold.value') or '',
104
'active': lambda *a: True,
105
'frequency': lambda *a: 3,
106
'consumption_method': lambda *a: 'amc',
107
'consumption_period_from': lambda *a: (now() + RelativeDate(day=1, months=-2)).strftime('%Y-%m-%d'),
108
'consumption_period_to': lambda *a: (now() + RelativeDate(day=1)).strftime('%Y-%m-%d'),
111
def copy(self, cr, uid, ids, defaults={}, context=None):
113
Increment the sequence
115
name = self.pool.get('ir.sequence').get(cr, uid, 'threshold.value') or ''
116
defaults.update({'name': name})
118
return super(threshold_value, self).copy(cr, uid, ids, defaults, context=context)
120
def default_get(self, cr, uid, fields, context=None):
122
Get the default values for the replenishment rule
124
res = super(threshold_value, self).default_get(cr, uid, fields, context=context)
126
company_id = res.get('company_id')
127
warehouse_id = res.get('warehouse_id')
129
if not 'company_id' in res:
130
company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.automatic.supply', context=context)
131
res.update({'company_id': company_id})
133
if not 'warehouse_id' in res:
134
warehouse_id = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)[0]
135
res.update({'warehouse_id': warehouse_id})
137
if not 'location_id' in res:
138
location_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_stock_id.id
139
res.update({'location_id': location_id})
143
def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
144
""" Finds default stock location id for changed warehouse.
145
@param warehouse_id: Changed id of warehouse.
146
@return: Dictionary of values.
149
w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
150
v = {'location_id': w.lot_stock_id.id}
154
def on_change_method(self, cr, uid, ids, method):
156
Unfill the consumption period if the method is FMC
160
if method and method == 'fmc':
161
res.update({'consumption_period_from': False, 'consumption_period_to': False})
162
elif method and method == 'amc':
163
res.update({'consumption_period_from': (now() + RelativeDate(day=1, months=-2)).strftime('%Y-%m-%d'),
164
'consumption_period_to': (now() + RelativeDate(day=1, months=1, days=-1)).strftime('%Y-%m-%d')})
166
return {'value': res}
168
def on_change_period(self, cr, uid, ids, from_date, to_date):
170
Check if the from date is younger than the to date
175
if from_date and to_date and from_date > to_date:
176
warn = {'title': 'Issue on date',
177
'message': 'The start date must be younger than end date'}
180
val.update({'consumption_period_from': (DateFrom(from_date) + RelativeDate(day=1)).strftime('%Y-%m-%d')})
183
val.update({'consumption_period_to': (DateFrom(to_date) + RelativeDate(months=1, day=1, days=-1)).strftime('%Y-%m-%d')})
185
return {'value': val, 'warning': warn}
187
##############################################################################################################################
188
# The code below aims to enable filtering products regarding their sublist or their nomenclature.
189
# Then, we fill lines of the one2many object 'threshold.value.line' according to the filtered products
190
##############################################################################################################################
191
def onChangeSearchNomenclature(self, cr, uid, id, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=True, context=None):
192
return self.pool.get('product.product').onChangeSearchNomenclature(cr, uid, 0, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, False, context={'withnum': 1})
194
def fill_lines(self, cr, uid, ids, context=None):
196
Fill all lines according to defined nomenclature level and sublist
200
for report in self.browse(cr, uid, ids, context=context):
205
# Get all products for the defined nomenclature
206
if report.nomen_manda_3:
207
nom = report.nomen_manda_3.id
208
field = 'nomen_manda_3'
209
elif report.nomen_manda_2:
210
nom = report.nomen_manda_2.id
211
field = 'nomen_manda_2'
212
elif report.nomen_manda_1:
213
nom = report.nomen_manda_1.id
214
field = 'nomen_manda_1'
215
elif report.nomen_manda_0:
216
nom = report.nomen_manda_0.id
217
field = 'nomen_manda_0'
219
product_ids.extend(self.pool.get('product.product').search(cr, uid, [(field, '=', nom)], context=context))
221
# Get all products for the defined list
222
if report.sublist_id:
223
for line in report.sublist_id.product_ids:
224
product_ids.append(line.name.id)
226
# Check if products in already existing lines are in domain
228
for line in report.line_ids:
229
if line.product_id.id in product_ids:
230
products.append(line.product_id.id)
232
self.pool.get('threshold.value.line').unlink(cr, uid, line.id, context=context)
234
for product in self.pool.get('product.product').browse(cr, uid, product_ids, context=context):
235
# Check if the product is not already on the report
236
if product.type not in ('consu', 'service', 'service_recep') and product.id not in products:
237
self.pool.get('threshold.value.line').create(cr, uid, {'product_id': product.id,
238
'product_uom_id': product.uom_id.id,
240
'threshold_value_id': report.id})
241
return {'type': 'ir.actions.act_window',
242
'res_model': 'threshold.value',
249
def dummy(self, cr, uid, ids, context=None):
252
def get_nomen(self, cr, uid, id, field):
253
return self.pool.get('product.nomenclature').get_nomen(cr, uid, self, id, field, context={'withnum': 1})
255
def write(self, cr, uid, ids, vals, context=None):
256
if vals.get('sublist_id',False):
257
vals.update({'nomen_manda_0':False,'nomen_manda_1':False,'nomen_manda_2':False,'nomen_manda_3':False})
258
if vals.get('nomen_manda_0',False):
259
vals.update({'sublist_id':False})
260
ret = super(threshold_value, self).write(cr, uid, ids, vals, context=context)
265
class threshold_value_line(osv.osv):
266
_name = 'threshold.value.line'
267
_description = 'Threshold Value Line'
268
_rec_name = 'product_id'
270
def copy_data(self, cr, uid, ids, defaults={}, context=None):
271
res = super(threshold_value_line, self).copy_data(cr, uid, ids, defaults, context=context)
273
if isinstance(res, dict):
274
if 'threshold_value_id' in res:
275
del res['threshold_value_id']
276
if 'threshold_value_id2' in res:
277
del res['threshold_value_id2']
281
def create(self, cr, uid, vals, context=None):
283
Add the second link to the threshold value rule
285
if 'threshold_value_id' in vals:
286
vals.update({'threshold_value_id2': vals['threshold_value_id']})
287
elif 'threshold_value_id2' in vals:
288
vals.update({'threshold_value_id': vals['threshold_value_id2']})
290
return super(threshold_value_line, self).create(cr, uid, vals, context=context)
292
def write(self, cr, uid, ids, vals, context=None):
294
Add the second link to the threshold value rule
296
if 'threshold_value_id' in vals:
297
vals.update({'threshold_value_id2': vals['threshold_value_id']})
298
elif 'threshold_value_id2' in vals:
299
vals.update({'threshold_value_id': vals['threshold_value_id2']})
301
context.update({'fake_threshold_value': vals.get('fake_threshold_value', False)})
302
vals.update({'fake_threshold_value': 0.00})
304
return super(threshold_value_line, self).write(cr, uid, ids, vals, context=context)
306
def _get_values(self, cr, uid, ids, field_name, arg, context=None):
308
Compute and return the threshold value and qty to order
314
for line in self.browse(cr, uid, ids, context=context):
315
if context.get('fake_threshold_value', False):
316
res[line.id] = context.get('fake_threshold_value', 0.00)
320
rule = line.threshold_value_id
321
context.update({'location_id': rule.location_id.id, 'compute_child': True})
322
product = self.pool.get('product.product').browse(cr, uid, line.product_id.id, context=context)
323
result = self._get_threshold_value(cr, uid, line.id, product, rule.compute_method, rule.consumption_method,
324
rule.consumption_period_from, rule.consumption_period_to, rule.frequency,
325
rule.safety_month, rule.lead_time, rule.supplier_lt, line.product_uom_id.id, context)
326
res[line.id] = result.get(field_name, 0.00)
331
def _get_threshold(self, cr, uid, ids, context={}):
333
for t in self.pool.get('threshold.value').browse(cr, uid, ids, context=context):
340
'product_id': fields.many2one('product.product', string='Product', required=True),
341
'product_uom_id': fields.many2one('product.uom', string='Product UoM', required=True),
342
'product_qty': fields.function(_get_values, method=True, type='float', string='Quantity to order'),
343
'fake_threshold_value': fields.float(digits=(16,2), string='Threshold value'),
344
'threshold_value': fields.function(_get_values, method=True, type='float', string='Threshold value',
345
store={'threshold.value.line': (lambda self, cr, uid, ids, c=None: ids, ['product_id'],20),
346
'threshold.value': (_get_threshold, ['compute_method',
347
'consumption_method',
348
'consumption_period_from',
349
'consumption_period_to',
353
'supplier_lt'], 10)}),
354
'fixed_product_qty': fields.float(digits=(16,2), string='Quantity to order'),
355
'fixed_threshold_value': fields.float(digits=(16,2), string='Threshold value'),
356
'threshold_value_id': fields.many2one('threshold.value', string='Threshold', ondelete='cascade', required=True),
357
'threshold_value_id2': fields.many2one('threshold.value', string='Threshold', ondelete='cascade', required=True)
360
def _check_uniqueness(self, cr, uid, ids, context=None):
362
Check if the product is not already in the current rule
364
for line in self.browse(cr, uid, ids, context=context):
365
lines = self.search(cr, uid, [('id', '!=', line.id),
366
('product_id', '=', line.product_id.id),
368
('threshold_value_id2', '=', line.threshold_value_id2.id),
369
('threshold_value_id', '=', line.threshold_value_id.id)], context=context)
376
(_check_uniqueness, 'You cannot have two times the same product on the same threshold value rule', ['product_id'])
379
def _get_threshold_value(self, cr, uid, line_id, product, compute_method, consumption_method,
380
consumption_period_from, consumption_period_to, frequency,
381
safety_month, lead_time, supplier_lt, uom_id, context=None):
383
Return the threshold value and ordered qty of a product line
389
threshold_value = 0.00
391
if compute_method == 'computed':
392
# Get the product available before change the context (from_date and to_date in context)
393
product_available = product.qty_available
395
# Change the context to compute consumption
397
c.update({'from_date': consumption_period_from, 'to_date': consumption_period_to})
398
product = self.pool.get('product.product').browse(cr, uid, product.id, context=c)
399
cons = consumption_method == 'fmc' and product.reviewed_consumption or product.product_amc
401
# Set lead time according to choices in threshold rule (supplier or manual lead time)
402
lt = supplier_lt and float(product.seller_delay)/30.0 or lead_time
404
# Compute the threshold value
405
threshold_value = cons * (lt + safety_month)
406
threshold_value = self.pool.get('product.uom')._compute_qty(cr, uid, product.uom_id.id, threshold_value, product.uom_id.id)
408
# Compute the quantity to re-order
409
qty_to_order = cons * (frequency + lt + safety_month)\
410
- product_available - product.incoming_qty + product.outgoing_qty
411
qty_to_order = self.pool.get('product.uom')._compute_qty(cr, uid, uom_id or product.uom_id.id, \
412
qty_to_order, product.uom_id.id)
413
qty_to_order = qty_to_order > 0.00 and qty_to_order or 0.00
415
line = self.browse(cr, uid, line_id, context=context)
416
threshold_value = line.fixed_threshold_value
417
qty_to_order = line.fixed_product_qty
419
return {'threshold_value': threshold_value, 'product_qty': qty_to_order}
421
def onchange_product_id(self, cr, uid, ids, product_id, compute_method=False, consumption_method=False,
422
consumption_period_from=False, consumption_period_to=False, frequency=False,
423
safety_month=False, lead_time=False, supplier_lt=False, context=None):
424
""" Finds UoM for changed product.
425
@param product_id: Changed id of product.
426
@return: Dictionary of values.
431
res = {'value': {'product_uom_id': False,
432
'fake_threshold_value': 0.00,
433
'threshold_value': 0.00}}
435
prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
436
res['value'].update({'product_uom_id': prod.uom_id.id})
439
tv = self._get_threshold_value(cr, uid, ids, prod, compute_method, consumption_method,
440
consumption_period_from, consumption_period_to, frequency,
441
safety_month, lead_time, supplier_lt, prod.uom_id.id, context=context)['threshold_value']
442
res['value'].update({'fake_threshold_value': tv, 'threshold_value': tv})
446
threshold_value_line()