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
25
from tools.translate import _
27
from mx.DateTime import DateFrom
28
from mx.DateTime import now
29
from mx.DateTime import RelativeDate
31
class threshold_value(osv.osv):
32
_name = 'threshold.value'
33
_description = 'Threshold value'
35
def _get_product_ids(self, cr, uid, ids, field_name, arg, context=None):
37
Returns a list of products for the rule
41
for rule in self.browse(cr, uid, ids, context=context):
43
for line in rule.line_ids:
44
res[rule.id].append(line.product_id.id)
48
def _src_product_ids(self, cr, uid, obj, name, args, context=None):
55
if arg[0] == 'product_ids':
57
line_ids = self.pool.get('threshold.value.line').search(cr, uid, [('product_id', arg[1], arg[2])])
58
for l in self.pool.get('threshold.value.line').browse(cr, uid, line_ids):
59
if l.threshold_value_id.id not in rule_ids:
60
rule_ids.append(l.threshold_value_id.id)
61
res.append(('id', 'in', rule_ids))
66
'name': fields.char(size=128, string='Reference', required=True),
67
'active': fields.boolean(string='Active'),
68
'warehouse_id': fields.many2one('stock.warehouse', string='Warehouse', required=True),
69
'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade",
70
domain="[('is_replenishment', '=', warehouse_id)]",
71
help='Location where the computation is made'),
72
'compute_method': fields.selection([('fixed', 'Fixed values'), ('computed', 'Computed values')],
73
string='Method of computation', required=True,
74
help="""If 'Fixed values', the scheduler will compare stock of product with the threshold value of the line. \n
75
If 'Computed values', the threshold value and the ordered quantity will be calculated according to defined parameters"""),
76
'consumption_method': fields.selection([('amc', 'Average Monthly Consumption'), ('fmc', 'Forecasted Monthly Consumption')],
77
string='Consumption Method',
78
help='Method used to compute the consumption of products.'),
79
'consumption_period_from': fields.date(string='Period of calculation',
80
help='This period is a number of past months the system has to consider for AMC calculation.'\
81
'By default this value is equal to the frequency in the Threshold.'),
82
'consumption_period_to': fields.date(string='-'),
83
'frequency': fields.float(digits=(16,2), string='Order frequency',
84
help='The time between two replenishments. Will be used to compute the quantity to order.'),
85
'safety_month': fields.float(digits=(16,2), string='Safety Stock in months',
86
help='In months. Period during the stock is not empty but need to be replenish. \
87
Used to compute the quantity to order.'),
88
'lead_time': fields.float(digits=(16,2), string='Fixed Lead Time in months',
89
help='In months. Time to be delivered after processing the purchase order.'),
90
'supplier_lt': fields.boolean(string='Product\'s supplier LT',
91
help='If checked, use the lead time set in the supplier form.'),
92
'line_ids': fields.one2many('threshold.value.line', 'threshold_value_id', string="Products"),
93
'fixed_line_ids': fields.one2many('threshold.value.line', 'threshold_value_id2', string="Products"),
94
'product_ids': fields.function(_get_product_ids, fnct_search=_src_product_ids,
95
type='many2many', relation='product.product', method=True, string='Products'),
96
'sublist_id': fields.many2one('product.list', string='List/Sublist', ondelete='set null'),
97
'nomen_manda_0': fields.many2one('product.nomenclature', 'Main Type', ondelete='set null'),
98
'nomen_manda_1': fields.many2one('product.nomenclature', 'Group', ondelete='set null'),
99
'nomen_manda_2': fields.many2one('product.nomenclature', 'Family', ondelete='set null'),
100
'nomen_manda_3': fields.many2one('product.nomenclature', 'Root', ondelete='set null'),
104
'name': lambda obj, cr, uid, context=None: obj.pool.get('ir.sequence').get(cr, uid, 'threshold.value') or '',
105
'active': lambda *a: True,
106
'frequency': lambda *a: 3,
107
'consumption_method': lambda *a: 'amc',
108
'consumption_period_from': lambda *a: (now() + RelativeDate(day=1, months=-2)).strftime('%Y-%m-%d'),
109
'consumption_period_to': lambda *a: (now() + RelativeDate(day=1)).strftime('%Y-%m-%d'),
112
def copy(self, cr, uid, ids, defaults={}, context=None):
114
Increment the sequence
116
name = self.pool.get('ir.sequence').get(cr, uid, 'threshold.value') or ''
117
defaults.update({'name': name})
119
return super(threshold_value, self).copy(cr, uid, ids, defaults, context=context)
121
def default_get(self, cr, uid, fields, context=None):
123
Get the default values for the replenishment rule
125
res = super(threshold_value, self).default_get(cr, uid, fields, context=context)
127
company_id = res.get('company_id')
128
warehouse_id = res.get('warehouse_id')
130
if not 'company_id' in res:
131
company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.automatic.supply', context=context)
132
res.update({'company_id': company_id})
134
if not 'warehouse_id' in res:
135
warehouse_id = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context)[0]
136
res.update({'warehouse_id': warehouse_id})
138
if not 'location_id' in res:
139
location_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_stock_id.id
140
res.update({'location_id': location_id})
144
def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
145
""" Finds default stock location id for changed warehouse.
146
@param warehouse_id: Changed id of warehouse.
147
@return: Dictionary of values.
150
w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
151
v = {'location_id': w.lot_stock_id.id}
155
def on_change_method(self, cr, uid, ids, method):
157
Unfill the consumption period if the method is FMC
161
if method and method == 'fmc':
162
res.update({'consumption_period_from': False, 'consumption_period_to': False})
163
elif method and method == 'amc':
164
res.update({'consumption_period_from': (now() + RelativeDate(day=1, months=-2)).strftime('%Y-%m-%d'),
165
'consumption_period_to': (now() + RelativeDate(day=1, months=1, days=-1)).strftime('%Y-%m-%d')})
167
return {'value': res}
169
def on_change_period(self, cr, uid, ids, from_date, to_date):
171
Check if the from date is younger than the to date
176
if from_date and to_date and from_date > to_date:
177
warn = {'title': 'Issue on date',
178
'message': 'The start date must be younger than end date'}
181
val.update({'consumption_period_from': (DateFrom(from_date) + RelativeDate(day=1)).strftime('%Y-%m-%d')})
184
val.update({'consumption_period_to': (DateFrom(to_date) + RelativeDate(months=1, day=1, days=-1)).strftime('%Y-%m-%d')})
186
return {'value': val, 'warning': warn}
188
##############################################################################################################################
189
# The code below aims to enable filtering products regarding their sublist or their nomenclature.
190
# Then, we fill lines of the one2many object 'threshold.value.line' according to the filtered products
191
##############################################################################################################################
192
def onChangeSearchNomenclature(self, cr, uid, id, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=True, context=None):
193
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})
195
def fill_lines(self, cr, uid, ids, context=None):
197
Fill all lines according to defined nomenclature level and sublist
201
for report in self.browse(cr, uid, ids, context=context):
206
# Get all products for the defined nomenclature
207
if report.nomen_manda_3:
208
nom = report.nomen_manda_3.id
209
field = 'nomen_manda_3'
210
elif report.nomen_manda_2:
211
nom = report.nomen_manda_2.id
212
field = 'nomen_manda_2'
213
elif report.nomen_manda_1:
214
nom = report.nomen_manda_1.id
215
field = 'nomen_manda_1'
216
elif report.nomen_manda_0:
217
nom = report.nomen_manda_0.id
218
field = 'nomen_manda_0'
220
product_ids.extend(self.pool.get('product.product').search(cr, uid, [(field, '=', nom)], context=context))
222
# Get all products for the defined list
223
if report.sublist_id:
224
for line in report.sublist_id.product_ids:
225
product_ids.append(line.name.id)
227
# Check if products in already existing lines are in domain
229
for line in report.line_ids:
230
if line.product_id.id in product_ids:
231
products.append(line.product_id.id)
233
self.pool.get('threshold.value.line').unlink(cr, uid, line.id, context=context)
235
for product in self.pool.get('product.product').browse(cr, uid, product_ids, context=context):
236
# Check if the product is not already on the report
237
if product.type not in ('consu', 'service', 'service_recep') and product.id not in products:
238
self.pool.get('threshold.value.line').create(cr, uid, {'product_id': product.id,
239
'product_uom_id': product.uom_id.id,
241
'threshold_value_id': report.id})
242
return {'type': 'ir.actions.act_window',
243
'res_model': 'threshold.value',
250
def dummy(self, cr, uid, ids, context=None):
253
def get_nomen(self, cr, uid, id, field):
254
return self.pool.get('product.nomenclature').get_nomen(cr, uid, self, id, field, context={'withnum': 1})
256
def write(self, cr, uid, ids, vals, context=None):
257
if vals.get('sublist_id',False):
258
vals.update({'nomen_manda_0':False,'nomen_manda_1':False,'nomen_manda_2':False,'nomen_manda_3':False})
259
if vals.get('nomen_manda_0',False):
260
vals.update({'sublist_id':False})
261
ret = super(threshold_value, self).write(cr, uid, ids, vals, context=context)
264
def on_change_compute_method(self, cr, uid, ids, compute_method,
267
if compute_method and compute_method == 'computed':
268
# UF-2511: switch from 'fixed' to 'compute' compute method
269
# warn user to refresh values
270
# (are not any more computed in 'fixed' mode)
271
msg = "You switch from 'fixed values' to 'computed values'. " \
272
"Please click on 'Refresh values' button to compute values."
275
'title': _('Warning'),
283
class threshold_value_line(osv.osv):
284
_name = 'threshold.value.line'
285
_description = 'Threshold Value Line'
286
_rec_name = 'product_id'
288
def copy_data(self, cr, uid, ids, defaults={}, context=None):
289
res = super(threshold_value_line, self).copy_data(cr, uid, ids, defaults, context=context)
291
if isinstance(res, dict):
292
if 'threshold_value_id' in res:
293
del res['threshold_value_id']
294
if 'threshold_value_id2' in res:
295
del res['threshold_value_id2']
299
def create(self, cr, uid, vals, context=None):
301
Add the second link to the threshold value rule
303
if 'threshold_value_id' in vals:
304
vals.update({'threshold_value_id2': vals['threshold_value_id']})
305
elif 'threshold_value_id2' in vals:
306
vals.update({'threshold_value_id': vals['threshold_value_id2']})
308
return super(threshold_value_line, self).create(cr, uid, vals, context=context)
310
def write(self, cr, uid, ids, vals, context=None):
312
Add the second link to the threshold value rule
314
if 'threshold_value_id' in vals:
315
vals.update({'threshold_value_id2': vals['threshold_value_id']})
316
elif 'threshold_value_id2' in vals:
317
vals.update({'threshold_value_id': vals['threshold_value_id2']})
319
context.update({'fake_threshold_value': vals.get('fake_threshold_value', False)})
320
vals.update({'fake_threshold_value': 0.00})
322
return super(threshold_value_line, self).write(cr, uid, ids, vals, context=context)
324
def _get_values(self, cr, uid, ids, field_name, arg, context=None):
326
Compute and return the threshold value and qty to order
332
for line in self.browse(cr, uid, ids, context=context):
333
if context.get('fake_threshold_value', False):
334
res[line.id] = context.get('fake_threshold_value', 0.00)
338
rule = line.threshold_value_id
339
context.update({'location_id': rule.location_id.id, 'compute_child': True})
340
product = self.pool.get('product.product').browse(cr, uid, line.product_id.id, context=context)
341
result = self._get_threshold_value(cr, uid, line.id, product, rule.compute_method, rule.consumption_method,
342
rule.consumption_period_from, rule.consumption_period_to, rule.frequency,
343
rule.safety_month, rule.lead_time, rule.supplier_lt, line.product_uom_id.id, context)
344
res[line.id] = result.get(field_name, 0.00)
349
def _get_threshold(self, cr, uid, ids, context={}):
351
for t in self.pool.get('threshold.value').browse(cr, uid, ids, context=context):
357
def _get_data(self, cr, uid, ids, field_name, args, context=None):
361
product_obj = self.pool.get('product.product')
362
proc_obj = self.pool.get('procurement.order')
364
if isinstance(ids, (int, long)):
369
for line in self.browse(cr, uid, ids, context=context):
370
if context and context.get('compute_method', False) == 'fixed':
371
# UF-2511: do not compute in 'fixed' compute method mode
375
'available_stock': 0.,
376
'expiry_before': False,
377
'supplier_id': False,
378
'required_date': False,
383
location_id = line.threshold_value_id.location_id.id
384
stock_product = product_obj.browse(cr, uid, line.product_id.id, context=dict(context, location=location_id))
386
from_date = line.threshold_value_id.consumption_period_from
387
to_date = line.threshold_value_id.consumption_period_to
388
consu_product = product_obj.browse(cr, uid, line.product_id.id, context=dict(context, from_date=from_date, to_date=to_date))
390
if line.threshold_value_id.consumption_method == 'amc':
391
consu = consu_product.product_amc
392
elif line.threshold_value_id.consumption_method == 'fmc':
393
consu = consu_product.reviewed_consumption
398
d_values = {'reviewed_consumption': line.threshold_value_id.consumption_method == 'fmc',
399
'past_consumption': line.threshold_value_id.consumption_method == 'amc',
400
'manual_consumption': 0.00,
401
'consumption_period_from': line.threshold_value_id.consumption_period_from,
402
'consumption_period_to': line.threshold_value_id.consumption_period_to,
403
'leadtime': line.threshold_value_id.lead_time,
404
'coverage': line.threshold_value_id.frequency,
405
'safety_stock': 0.00,
406
'safety_time': line.threshold_value_id.safety_month}
407
expiry_product_qty = product_obj.get_expiry_qty(cr, uid, line.product_id.id, location_id, False, d_values, context=dict(context, location=location_id, compute_child=True))
409
new_context = context.copy()
410
new_context.update({'from_date': from_date,
413
'consumption_period_from': d_values['consumption_period_from'],
414
'consumption_period_to': d_values['consumption_period_to'],})
416
qty_to_order, req_date = proc_obj._compute_quantity(cr, uid, False, line.product_id, line.threshold_value_id.location_id.id, d_values, context=new_context)
418
res[line.id] = {'consumption': consu,
419
'real_stock': stock_product.qty_available,
420
'available_stock': stock_product.virtual_available,
421
'expiry_before': expiry_product_qty,
422
'supplier_id': stock_product.seller_id.id,
423
'required_date': req_date,
429
'product_id': fields.many2one('product.product', string='Product', required=True),
430
'product_uom_id': fields.many2one('product.uom', string='Product UoM', required=True),
431
'product_qty': fields.function(_get_values, method=True, type='float', string='Quantity to order'),
432
'fake_threshold_value': fields.float(digits=(16,2), string='Threshold value'),
433
'threshold_value': fields.function(_get_values, method=True, type='float', string='Threshold value',
434
store={'threshold.value.line': (lambda self, cr, uid, ids, c=None: ids, ['product_id'],20),
435
'threshold.value': (_get_threshold, ['compute_method',
436
'consumption_method',
437
'consumption_period_from',
438
'consumption_period_to',
442
'supplier_lt'], 10)}),
443
'fixed_product_qty': fields.float(digits=(16,2), string='Quantity to order'),
444
'fixed_threshold_value': fields.float(digits=(16,2), string='Threshold value'),
445
'threshold_value_id': fields.many2one('threshold.value', string='Threshold', ondelete='cascade', required=True),
446
'threshold_value_id2': fields.many2one('threshold.value', string='Threshold', ondelete='cascade', required=True),
447
'consumption': fields.function(_get_data, method=True, type='float', digits=(16,3), string='AMC/FMC', multi='data', readonly=True),
448
'real_stock': fields.function(_get_data, method=True, type='float', digits=(16,3), string='Real stock', multi='data', readonly=True),
449
'available_stock': fields.function(_get_data, method=True, type='float', digits=(16,3), string='Available stock', multi='data', readonly=True),
450
'expiry_before': fields.function(_get_data, method=True, type='float', digits=(16,3), string='Exp. before consumption', multi='data', readonly=True),
451
'supplier_id': fields.function(_get_data, method=True, type='many2one', relation='res.partner', string='Supplier', multi='data', readonly=True),
452
'required_date': fields.function(_get_data, method=True, type='date', string='Required by date', multi='data', readonly=True),
455
def _check_uniqueness(self, cr, uid, ids, context=None):
457
Check if the product is not already in the current rule
459
for line in self.browse(cr, uid, ids, context=context):
460
lines = self.search(cr, uid, [('id', '!=', line.id),
461
('product_id', '=', line.product_id.id),
463
('threshold_value_id2', '=', line.threshold_value_id2.id),
464
('threshold_value_id', '=', line.threshold_value_id.id)], context=context)
471
(_check_uniqueness, 'You cannot have two times the same product on the same threshold value rule', ['product_id'])
474
def _get_threshold_value(self, cr, uid, line_id, product, compute_method, consumption_method,
475
consumption_period_from, consumption_period_to, frequency,
476
safety_month, lead_time, supplier_lt, uom_id, context=None):
478
Return the threshold value and ordered qty of a product line
483
if line_id and isinstance(line_id, list):
487
threshold_value = 0.00
489
if compute_method == 'computed':
490
# Get the product available before change the context (from_date and to_date in context)
491
product_available = product.qty_available
493
# Change the context to compute consumption
495
c.update({'from_date': consumption_period_from, 'to_date': consumption_period_to})
496
product = self.pool.get('product.product').browse(cr, uid, product.id, context=c)
497
cons = consumption_method == 'fmc' and product.reviewed_consumption or product.product_amc
499
# Set lead time according to choices in threshold rule (supplier or manual lead time)
500
lt = supplier_lt and float(product.seller_delay)/30.0 or lead_time
502
# Compute the threshold value
503
threshold_value = cons * (lt + safety_month)
504
threshold_value = self.pool.get('product.uom')._compute_qty(cr, uid, product.uom_id.id, threshold_value, product.uom_id.id)
506
# Compute the quantity to re-order
507
qty_to_order = cons * (frequency + lt + safety_month)\
508
- product_available - product.incoming_qty + product.outgoing_qty
509
qty_to_order = self.pool.get('product.uom')._compute_qty(cr, uid, uom_id or product.uom_id.id, \
510
qty_to_order, product.uom_id.id)
511
qty_to_order = qty_to_order > 0.00 and qty_to_order or 0.00
513
line = self.browse(cr, uid, line_id, context=context)
514
threshold_value = line.fixed_threshold_value
515
qty_to_order = line.fixed_product_qty
517
return {'threshold_value': threshold_value, 'product_qty': qty_to_order}
519
def onchange_product_id(self, cr, uid, ids, product_id, compute_method=False, consumption_method=False,
520
consumption_period_from=False, consumption_period_to=False, frequency=False,
521
safety_month=False, lead_time=False, supplier_lt=False, fixed_tv=0.00,
522
fixed_qty=0.00, uom_id=False, field='product_id', context=None):
523
""" Finds UoM for changed product.
524
@param product_id: Changed id of product.
525
@return: Dictionary of values.
530
res = {'value': {'product_uom_id': False,
531
'fake_threshold_value': 0.00,
532
'threshold_value': 0.00}}
534
prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
535
if field == 'product_id':
536
res['value'].update({'product_uom_id': prod.uom_id.id})
538
res['value'].update({'product_uom_id': uom_id})
541
tv = self._get_threshold_value(cr, uid, ids, prod, compute_method, consumption_method,
542
consumption_period_from, consumption_period_to, frequency,
543
safety_month, lead_time, supplier_lt, uom_id or prod.uom_id.id, context=context)['threshold_value']
544
res['value'].update({'fake_threshold_value': tv, 'threshold_value': tv})
547
res = self.pool.get('product.uom')._change_round_up_qty(cr, uid, uom_id or prod.uom_id.id, tv, ['fixed_threshold_value', 'fixed_product_qty', 'threshold_value', 'fake_threshold_value'], result=res)
548
if prod.uom_id.id and fixed_tv:
549
res = self.pool.get('product.uom')._change_round_up_qty(cr, uid, uom_id or prod.uom_id.id, fixed_tv, ['fixed_threshold_value'], result=res)
550
if prod.uom_id.id and fixed_qty:
551
res = self.pool.get('product.uom')._change_round_up_qty(cr, uid, uom_id or prod.uom_id.id, fixed_tv, ['fixed_product_qty'], result=res)
555
def onchange_uom_qty(self, cr, uid, ids, uom_id, tv_qty, product_qty):
557
Check round of qty according to UoM
560
uom_obj = self.pool.get('product.uom')
563
res = uom_obj._change_round_up_qty(cr, uid, uom_id, tv_qty, 'fixed_threshold_value', result=res)
566
res = uom_obj._change_round_up_qty(cr, uid, uom_id, product_qty, 'fixed_product_qty', result=res)
570
threshold_value_line()