1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 TeMPO Consulting, MSF
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Affero General Public License as
9
# published by the Free Software Foundation, either version 3 of the
10
# License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Affero General Public License for more details.
17
# You should have received a copy of the GNU Affero General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
23
from osv import fields
24
from mx.DateTime import *
26
from tools.translate import _
33
from tempfile import TemporaryFile
34
from product._common import rounding
35
from spreadsheet_xml.spreadsheet_xml_write import SpreadsheetCreator
38
def _get_asset_mandatory(product):
39
return product.type == 'product' and product.subtype == 'asset'
42
class real_average_consumption(osv.osv):
43
_name = 'real.average.consumption'
44
_description = 'Real Average Consumption'
46
def _get_nb_lines(self, cr, uid, ids, field_name, args, context=None):
48
Returns the # of lines on the real average consumption
52
for mrc in self.browse(cr, uid, ids, context=context):
53
res[mrc.id] = len(mrc.line_ids)
57
def _check_active_product(self, cr, uid, ids, context=None):
59
Check if the real consumption report contains a line with an inactive product
61
inactive_lines = self.pool.get('real.average.consumption.line').search(cr, uid, [
62
('product_id.active', '=', False),
63
('rac_id', 'in', ids),
64
('rac_id.created_ok', '=', True)
68
plural = len(inactive_lines) == 1 and _('A product has') or _('Some products have')
69
l_plural = len(inactive_lines) == 1 and _('line') or _('lines')
70
p_plural = len(inactive_lines) == 1 and _('this inactive product') or _('those inactive products')
71
raise osv.except_osv(_('Error'), _('%s been inactivated. If you want to validate this document you have to remove/correct the %s containing %s (see red %s of the document)') % (plural, l_plural, p_plural, l_plural))
75
def unlink(self, cr, uid, ids, context=None):
77
Display a message to the user if the report has been confirmed
78
and stock moves has been generated
80
for report in self.browse(cr, uid, ids, context=context):
81
if report.state == 'done':
82
raise osv.except_osv(_('Error'), _('This report is closed. You cannot delete it !'))
83
if report.created_ok and report.picking_id:
84
if report.picking_id.state != 'cancel':
85
raise osv.except_osv(_('Error'), _('You cannot delete this report because stock moves has been generated and validated from this report !'))
87
for move in report.picking_id.move_lines:
88
if move.state != 'cancel':
89
raise osv.except_osv(_('Error'), _('You cannot delete this report because stock moves has been generated and validated from this report !'))
91
return super(real_average_consumption, self).unlink(cr, uid, ids, context=context)
93
def copy(self, cr, uid, ids, default=None, context=None):
95
Unvalidate all lines of the duplicate report
97
# Change default values
102
if not 'picking_id' in default:
103
default['picking_id'] = False
105
default['name'] = self.pool.get('ir.sequence').get(cr, uid, 'consumption.report')
108
res = super(real_average_consumption, self).copy(cr, uid, ids, default, context=context)
110
# Unvalidate all lines of the report
111
for report in self.browse(cr, uid, [res], context=context):
113
for line in report.line_ids:
114
lines.append(line.id)
116
self.pool.get('real.average.consumption.line').write(cr, uid, lines, {'move_id': False}, context=context)
118
# update created_ok at this end to disable _check qty on line
119
self.write(cr, uid, res, {'created_ok': False})
120
self.button_update_stock(cr, uid, res)
124
def get_bool_values(self, cr, uid, ids, fields, arg, context=None):
126
if isinstance(ids, (int, long)):
128
for obj in self.browse(cr, uid, ids, context=context):
130
if any([item for item in obj.line_ids if item.to_correct_ok]):
135
'name': fields.char(size=64, string='Reference'),
136
'creation_date': fields.datetime(string='Creation date', required=1),
137
'cons_location_id': fields.many2one('stock.location', string='Consumer location', domain=[('usage', '=', 'internal')], required=True),
138
'activity_id': fields.many2one('stock.location', string='Activity', domain=[('usage', '=', 'customer')], required=1),
139
'period_from': fields.date(string='Period from', required=True),
140
'period_to': fields.date(string='Period to', required=True),
141
'sublist_id': fields.many2one('product.list', string='List/Sublist'),
142
'line_ids': fields.one2many('real.average.consumption.line', 'rac_id', string='Lines'),
143
'picking_id': fields.many2one('stock.picking', string='Picking', readonly=True),
144
'created_ok': fields.boolean(string='Out moves created'),
145
'nb_lines': fields.function(_get_nb_lines, method=True, type='integer', string='# lines', readonly=True,),
146
'nomen_manda_0': fields.many2one('product.nomenclature', 'Main Type'),
147
'nomen_manda_1': fields.many2one('product.nomenclature', 'Group'),
148
'nomen_manda_2': fields.many2one('product.nomenclature', 'Family'),
149
'nomen_manda_3': fields.many2one('product.nomenclature', 'Root'),
150
'hide_column_error_ok': fields.function(get_bool_values, method=True, readonly=True, type="boolean", string="Show column errors", store=False),
151
'state': fields.selection([('draft', 'Draft'), ('done', 'Closed'),('cancel','Cancelled')], string="State", readonly=True),
155
#'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'consumption.report'),
156
'creation_date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
157
'activity_id': lambda obj, cr, uid, context: obj.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_internal_customers')[1],
158
'period_to': lambda *a: time.strftime('%Y-%m-%d'),
159
'nb_lines': lambda *a: 0,
160
'state': lambda *a: 'draft',
164
('date_coherence', "check (period_from <= period_to)", '"Period from" must be less than or equal to "Period to"'),
168
(_check_active_product, "You cannot confirm this real consumption report because it contains a line with an inactive product", ['line_ids', 'created_ok']),
171
def create(self, cr, uid, vals, context=None):
173
Add name of the report at creation
178
if not 'name' in vals:
179
vals.update({'name': self.pool.get('ir.sequence').get(cr, uid, 'consumption.report')})
181
return super(real_average_consumption, self).create(cr, uid, vals, context=context)
183
def change_cons_location_id(self, cr, uid, ids, context=None):
185
Open the wizard to change the location
187
wiz_id = self.pool.get('real.consumption.change.location').create(cr, uid, {'report_id': ids[0]}, context=context)
188
return {'type': 'ir.actions.act_window',
189
'res_model': 'real.consumption.change.location',
195
def button_update_stock(self, cr, uid, ids, context=None):
196
if isinstance(ids, (int, long)):
199
for line in self.read(cr, uid, ids, ['created_ok','line_ids']):
200
if line['created_ok']:
202
to_update += line['line_ids']
205
self.pool.get('real.average.consumption.line')._check_qty(cr, uid, to_update, {'noraise': True})
208
def save_and_process(self, cr, uid, ids, context=None):
210
Returns the wizard to confirm the process of all lines
214
self.check_lines_to_fix(cr, uid, ids, context)
215
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'consumption_calculation', 'real_average_consumption_confirmation_view')[1],
217
return {'type': 'ir.actions.act_window',
218
'res_model': 'real.average.consumption',
222
'view_id': [view_id],
226
def draft_button(self, cr, uid, ids, context=None):
227
if isinstance(ids, (int, long)):
232
self.write(cr, uid, ids, {'state':'draft'}, context=context)
234
return {'type': 'ir.actions.act_window',
235
'res_model': 'real.average.consumption',
237
'view_mode': 'form,tree',
242
def cancel_button(self, cr, uid, ids, context=None):
243
if isinstance(ids, (int, long)):
248
self.write(cr, uid, ids, {'state':'cancel'}, context=context)
250
return {'type': 'ir.actions.act_window',
251
'res_model': 'real.average.consumption',
253
'view_mode': 'form,tree',
259
def process_moves(self, cr, uid, ids, context=None):
261
Creates all stock moves according to the report lines
263
if isinstance(ids, (int, long)):
268
move_obj = self.pool.get('stock.move')
269
line_obj = self.pool.get('real.average.consumption.line')
270
wf_service = netsvc.LocalService("workflow")
272
reason_type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_consumption_report')[1]
276
# check and update lines
277
for rac in self.browse(cr, uid, ids, context=context):
278
if DateFrom(rac.period_to) > now():
279
raise osv.except_osv(_('Error'), _('"Period to" can\'t be in the future.'))
282
return {'type': 'ir.actions.close_window'}
283
line_obj._check_qty(cr, uid, [x.id for x in rac.line_ids])
285
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.partner_id.id
286
addresses = self.pool.get('res.partner').address_get(cr, uid, partner_id, ['delivery', 'default'])
287
address_id = addresses.get('delivery') or addresses.get('default')
289
for rac in self.browse(cr, uid, ids, context=context):
290
date = '%s %s'%(rac.period_to, time.strftime('%H:%M:%S'))
291
picking_id = self.pool.get('stock.picking').create(cr, uid, {'name': 'OUT-%s' % rac.name,
293
'partner_id': partner_id,
294
'address_id': address_id,
296
'subtype': 'standard',
299
'invoice_state': 'none',
301
'reason_type_id': reason_type_id}, context=context)
303
self.write(cr, uid, [rac.id], {'created_ok': True}, context=context)
304
for line in rac.line_ids:
305
if line.consumed_qty != 0.00:
306
move_id = move_obj.create(cr, uid, {'name': '%s/%s' % (rac.name, line.product_id.name),
307
'picking_id': picking_id,
308
'product_uom': line.uom_id.id,
309
'product_id': line.product_id.id,
310
'date_expected': date,
312
'product_qty': line.consumed_qty,
313
'prodlot_id': line.prodlot_id.id,
314
'expiry_date': line.expiry_date,
315
'asset_id': line.asset_id.id,
316
'location_id': rac.cons_location_id.id,
317
'location_dest_id': rac.activity_id.id,
319
'reason_type_id': reason_type_id})
320
move_ids.append(move_id)
321
line_obj.write(cr, uid, [line.id], {'move_id': move_id})
323
self.write(cr, uid, [rac.id], {'picking_id': picking_id, 'state': 'done'}, context=context)
325
# Confirm the picking
326
wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
329
move_obj.action_done(cr, uid, move_ids, context=context)
330
#move_obj.write(cr, uid, move_ids, {'date': rac.period_to}, context=context)
333
return {'type': 'ir.actions.act_window',
334
'res_model': 'real.average.consumption',
336
'view_mode': 'form,tree',
341
def import_rac(self, cr, uid, ids, context=None):
343
Launches the wizard to import lines from a file
347
context.update({'active_id': ids[0]})
349
return {'type': 'ir.actions.act_window',
350
'res_model': 'wizard.import.rac',
357
def export_rac(self, cr, uid, ids, context=None):
359
Creates an XML file and launches the wizard to save it
363
rac = self.browse(cr, uid, ids[0], context=context)
364
header_columns = [(_('Product Code'), 'string'), (_('Product Description'),'string'),(_('Product UoM'), 'string'), (_('Batch Number'),'string'), (_('Expiry Date'), 'string'), (_('Asset'), 'string'), (_('Consumed Qty'),'string'), (_('Remark'), 'string')]
366
for line in rac.line_ids:
367
list_of_lines.append([line.product_id.default_code, line.product_id.name, line.uom_id.name, line.prodlot_id and line.prodlot_id.name,
368
line.expiry_date and strptime(line.expiry_date,'%Y-%m-%d').strftime('%d/%m/%Y') or '', line.asset_id and line.asset_id.name, line.consumed_qty, line.remark or ''])
369
instanciate_class = SpreadsheetCreator('RAC', header_columns, list_of_lines)
370
file = base64.encodestring(instanciate_class.get_xml(default_filters=['decode.utf8']))
372
export_id = self.pool.get('wizard.export.rac').create(cr, uid, {'rac_id': ids[0],
374
'filename': 'rac_%s.xls' % (rac.cons_location_id.name.replace(' ', '_')),
375
'message': _('The RAC lines has been exported. Please click on Save As button to download the file'),}, context=context)
377
return {'type': 'ir.actions.act_window',
378
'res_model': 'wizard.export.rac',
386
def fill_lines(self, cr, uid, ids, context=None):
388
Fill all lines according to defined nomenclature level and sublist
392
self.write(cr, uid, ids, {'created_ok': True})
393
for report in self.browse(cr, uid, ids, context=context):
398
# Get all products for the defined nomenclature
399
if report.nomen_manda_3:
400
nom = report.nomen_manda_3.id
401
field = 'nomen_manda_3'
402
elif report.nomen_manda_2:
403
nom = report.nomen_manda_2.id
404
field = 'nomen_manda_2'
405
elif report.nomen_manda_1:
406
nom = report.nomen_manda_1.id
407
field = 'nomen_manda_1'
408
elif report.nomen_manda_0:
409
nom = report.nomen_manda_0.id
410
field = 'nomen_manda_0'
412
product_ids.extend(self.pool.get('product.product').search(cr, uid, [(field, '=', nom)], context=context))
414
# Get all products for the defined list
415
if report.sublist_id:
416
for line in report.sublist_id.product_ids:
417
product_ids.append(line.name.id)
419
# Check if products in already existing lines are in domain
421
for line in report.line_ids:
422
if line.product_id.id in product_ids:
423
products.append(line.product_id.id)
425
self.pool.get('real.average.consumption.line').unlink(cr, uid, line.id, context=context)
427
for product in self.pool.get('product.product').browse(cr, uid, product_ids, context=context):
428
# Check if the product is not already on the report
429
if product.id not in products:
430
batch_mandatory = product.batch_management
431
date_mandatory = product.perishable
432
asset_mandatory = _get_asset_mandatory(product)
433
values = {'product_id': product.id,
434
'uom_id': product.uom_id.id,
435
'consumed_qty': 0.00,
436
'batch_mandatory': batch_mandatory,
437
'date_mandatory': date_mandatory,
438
'asset_mandatory': asset_mandatory,
439
'rac_id': report.id,}
440
v = self.pool.get('real.average.consumption.line').product_onchange(cr, uid, [], product.id, report.cons_location_id.id,
441
product.uom_id.id, False, context=context)['value']
444
values.update({'remark': _('You must assign a batch number')})
446
values.update({'remark': _('You must assign an expiry date')})
448
values.update({'remark': _('You must assign an asset')})
449
self.pool.get('real.average.consumption.line').create(cr, uid, values)
451
self.write(cr, uid, ids, {'created_ok': False})
452
return {'type': 'ir.actions.act_window',
453
'res_model': 'real.average.consumption',
460
def get_nomen(self, cr, uid, id, field):
461
return self.pool.get('product.nomenclature').get_nomen(cr, uid, self, id, field, context={'withnum': 1})
463
def onChangeSearchNomenclature(self, cr, uid, id, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=True, context=None):
464
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})
466
def write(self, cr, uid, ids, vals, context=None):
467
if vals.get('sublist_id',False):
468
vals.update({'nomen_manda_0':False,'nomen_manda_1':False,'nomen_manda_2':False,'nomen_manda_3':False})
469
if vals.get('nomen_manda_0',False):
470
vals.update({'sublist_id':False})
471
ret = super(real_average_consumption, self).write(cr, uid, ids, vals, context=context)
474
def button_remove_lines(self, cr, uid, ids, context=None):
480
if isinstance(ids, (int, long)):
483
vals['line_ids'] = []
484
for line in self.browse(cr, uid, ids, context=context):
485
line_browse_list = line.line_ids
486
for var in line_browse_list:
487
vals['line_ids'].append((2, var.id))
488
self.write(cr, uid, ids, vals, context=context)
491
def check_lines_to_fix(self, cr, uid, ids, context=None):
493
Check the lines that need to be corrected
495
if isinstance(ids, (int, long)):
497
obj_data = self.pool.get('ir.model.data')
498
product_tbd = obj_data.get_object_reference(cr, uid, 'msf_doc_import', 'product_tbd')[1]
499
uom_tbd = obj_data.get_object_reference(cr, uid, 'msf_doc_import', 'uom_tbd')[1]
501
for var in self.browse(cr, uid, ids, context=context):
502
# we check the lines that need to be fixed
504
for var in var.line_ids:
505
if var.consumed_qty and var.to_correct_ok:
506
raise osv.except_osv(_('Warning !'), _('Some lines need to be fixed before.'))
508
self.pool.get('real.average.consumption.line').write(cr, uid, var.id, {},context)
511
real_average_consumption()
514
class real_average_consumption_line(osv.osv):
515
_name = 'real.average.consumption.line'
516
_description = 'Real average consumption line'
517
_rec_name = 'product_id'
520
def _get_checks_all(self, cr, uid, ids, name, arg, context=None):
523
result[id] = {'batch_number_check': False, 'expiry_date_check': False, 'type_check': False, 'to_correct_ok': False}
525
for out in self.browse(cr, uid, ids, context=context):
527
result[out.id]['batch_number_check'] = out.product_id.batch_management
528
result[out.id]['expiry_date_check'] = out.product_id.perishable
529
result[out.id]['asset_check'] = _get_asset_mandatory(out.product_id)
530
# the lines with to_correct_ok=True will be red
532
result[out.id]['to_correct_ok'] = True
535
def _get_qty(self, cr, uid, product, lot, location, uom):
536
if not product and not lot:
538
context = {'location_id': location, 'location': location, 'uom': uom, 'compute_child': False}
540
return self.pool.get('product.product').read(cr, uid, product, ['qty_available'], context=context)['qty_available']
542
return self.pool.get('stock.production.lot').read(cr, uid, lot, ['stock_available'], context=context)['stock_available']
544
def _check_qty(self, cr, uid, ids, context=None):
548
noraise = context.get('noraise')
549
context.update({'error_message': ''})
550
if isinstance(ids, (int, long)):
553
for obj in self.browse(cr, uid, ids):
554
if obj.rac_id.created_ok:
557
# Prevent negative consumption qty.
558
if obj.consumed_qty < 0.00:
560
raise osv.except_osv(_('Error'), _('The consumed qty. must be positive or 0.00'))
561
elif context.get('import_in_progress'):
562
error_message.append(_('The consumed qty. must be positive or 0.00'))
563
context.update({'error_message': error_message})
565
location = obj.rac_id.cons_location_id.id
570
batch_mandatory = obj.product_id.batch_management
571
date_mandatory = obj.product_id.perishable
572
asset_mandatory = _get_asset_mandatory(obj.product_id)
574
if batch_mandatory and obj.consumed_qty != 0.00:
575
if not obj.prodlot_id:
577
raise osv.except_osv(_('Error'),
578
_("Product: %s, You must assign a Batch Number to process it.")%(obj.product_id.name,))
579
elif context.get('import_in_progress'):
580
error_message.append(_("You must assign a Batch Number to process it."))
581
context.update({'error_message': error_message})
583
prodlot_id = obj.prodlot_id.id
584
expiry_date = obj.prodlot_id.life_date
586
if date_mandatory and not batch_mandatory and obj.consumed_qty != 0.00:
587
prod_ids = self.pool.get('stock.production.lot').search(cr, uid, [('life_date', '=', obj.expiry_date),
588
('type', '=', 'internal'),
589
('product_id', '=', obj.product_id.id)], context=context)
590
expiry_date = obj.expiry_date or None # None because else it is False and a date can't have a boolean value
593
raise osv.except_osv(_('Error'),
594
_("Product: %s, no internal batch found for expiry (%s)")%(obj.product_id.name, obj.expiry_date or _('No expiry date set')))
595
elif context.get('import_in_progress'):
596
error_message.append(_("Line %s of the imported file: no internal batch number found for ED %s (please correct the data)"
597
) % (context.get('line_num', False), expiry_date and strptime(expiry_date, '%Y-%m-%d').strftime('%d-%m-%Y')))
598
context.update({'error_message': error_message})
600
prodlot_id = prod_ids[0]
602
if asset_mandatory and obj.consumed_qty != 0.00:
605
raise osv.except_osv(_('Error'),
606
_("Product: %s, You must assign an Asset to process it.")%(obj.product_id.name,))
607
elif context.get('import_in_progress'):
608
error_message.append(_("You must assign an Asset to process it."))
609
context.update({'error_message': error_message})
611
asset_id = obj.asset_id.id
613
product_qty = self._get_qty(cr, uid, obj.product_id.id, prodlot_id, location, obj.uom_id and obj.uom_id.id)
615
if prodlot_id and obj.consumed_qty > product_qty:
617
raise osv.except_osv(_('Error'),
618
_("Product: %s, Qty Consumed (%s) can't be greater than the Indicative Stock (%s)")%(obj.product_id.name, obj.consumed_qty, product_qty))
619
elif context.get('import_in_progress'):
620
error_message.append(_("Line %s of the imported file: Qty Consumed (%s) can't be greater than the Indicative Stock (%s)") % (context.get('line_num', False), obj.consumed_qty, product_qty))
621
context.update({'error_message': error_message})
622
# uf-1344 "quantity NOT in stock with this ED => line should be in red, no batch picked up"
624
#recursion: can't use write
625
cr.execute('UPDATE '+self._table+' SET product_qty=%s, batch_mandatory=%s, date_mandatory=%s, asset_mandatory=%s, prodlot_id=%s, expiry_date=%s, asset_id=%s where id=%s', (product_qty, batch_mandatory, date_mandatory, asset_mandatory, prodlot_id, expiry_date, asset_id, obj.id))
629
def _get_product(self, cr, uid, ids, context=None):
630
return self.pool.get('real.average.consumption.line').search(cr, uid, [('product_id', 'in', ids)], context=context)
632
def _get_inactive_product(self, cr, uid, ids, field_name, args, context=None):
634
Fill the error message if the product of the line is inactive
637
for line in self.browse(cr, uid, ids, context=context):
638
res[line.id] = {'inactive_product': False,
639
'inactive_error': ''}
640
if line.product_id and not line.product_id.active:
642
'inactive_product': True,
643
'inactive_error': _('The product in line is inactive !')
649
'product_id': fields.many2one('product.product', string='Product', required=True),
650
'ref': fields.related('product_id', 'default_code', type='char', size=64, readonly=True,
651
store={'product.product': (_get_product, ['default_code'], 10),
652
'real.average.consumption.line': (lambda self, cr, uid, ids, c=None: ids, ['product_id'], 20)}),
653
'uom_id': fields.many2one('product.uom', string='UoM', required=True),
654
'product_qty': fields.float(digits=(16,2), string='Indicative stock', readonly=True),
655
'consumed_qty': fields.float(digits=(16,2), string='Qty consumed', required=True),
656
'batch_number_check': fields.function(_get_checks_all, method=True, string='Batch Number Check', type='boolean', readonly=True, multi="m"),
657
'expiry_date_check': fields.function(_get_checks_all, method=True, string='Expiry Date Check', type='boolean', readonly=True, multi="m"),
658
'asset_check': fields.function(_get_checks_all, method=True, string='Asset Check', type='boolean', readonly=True, multi="m"),
659
'prodlot_id': fields.many2one('stock.production.lot', string='Batch number'),
660
'batch_mandatory': fields.boolean(string='BM'),
661
'expiry_date': fields.date(string='Expiry date'),
662
'date_mandatory': fields.boolean(string='DM'),
663
'asset_id': fields.char(size=32, string='Asset'),
664
'asset_mandatory': fields.boolean('AM'),
665
'remark': fields.char(size=256, string='Remark'),
666
'move_id': fields.many2one('stock.move', string='Move'),
667
'rac_id': fields.many2one('real.average.consumption', string='RAC', ondelete='cascade'),
668
'text_error': fields.text('Errors', readonly=True),
669
'to_correct_ok': fields.function(_get_checks_all, method=True, type="boolean", string="To correct", store=False, readonly=True, multi="m"),
670
'just_info_ok': fields.boolean(string='Just for info'),
671
'inactive_product': fields.function(_get_inactive_product, method=True, type='boolean', string='Product is inactive', store=False, multi='inactive'),
672
'inactive_error': fields.function(_get_inactive_product, method=True, type='char', string='Comment', store=False, multi='inactive'),
676
'inactive_product': False,
677
'inactive_error': lambda *a: '',
680
# uf-1344 => need to pass the context so we use create and write instead
682
# (_check_qty, "The Qty Consumed can't be greater than the Indicative Stock", ['consumed_qty']),
686
('unique_lot_poduct', "unique(product_id, prodlot_id, rac_id)", 'The couple product, batch number has to be unique'),
689
def create(self, cr, uid, vals=None, context=None):
695
res = super(real_average_consumption_line, self).create(cr, uid, vals, context=context)
696
check = self._check_qty(cr, uid, res, context)
698
raise osv.except_osv(_('Error'), _('The Qty Consumed cant\'t be greater than the Indicative Stock'))
699
if vals.get('uom_id') and vals.get('product_id'):
700
product_id = vals.get('product_id')
701
product_uom = vals.get('uom_id')
702
if not self.pool.get('uom.tools').check_uom(cr, uid, product_id, product_uom, context):
703
raise osv.except_osv(_('Warning !'), _("You have to select a product UOM in the same category than the purchase UOM of the product"))
706
def write(self, cr, uid, ids, vals, context=None):
709
if isinstance(ids, (int, long)):
711
if not context.get('import_in_progress') and not context.get('button'):
712
obj_data = self.pool.get('ir.model.data')
713
tbd_uom = obj_data.get_object_reference(cr, uid, 'msf_doc_import', 'uom_tbd')[1]
714
tbd_product = obj_data.get_object_reference(cr, uid, 'msf_doc_import', 'product_tbd')[1]
716
if vals.get('uom_id'):
717
if vals.get('uom_id') == tbd_uom:
718
message += _('You have to define a valid UOM, i.e. not "To be define".')
719
if vals.get('product_id'):
720
if vals.get('product_id') == tbd_product:
721
message += _('You have to define a valid product, i.e. not "To be define".')
722
if vals.get('uom_id') and vals.get('product_id'):
723
product_id = vals.get('product_id')
724
product_uom = vals.get('uom_id')
725
if not self.pool.get('uom.tools').check_uom(cr, uid, product_id, product_uom, context):
726
message += _("You have to select a product UOM in the same category than the purchase UOM of the product")
728
raise osv.except_osv(_('Warning !'), message)
730
vals['text_error'] = False
731
res = super(real_average_consumption_line, self).write(cr, uid, ids, vals, context=context)
732
check = self._check_qty(cr, uid, ids, context)
734
raise osv.except_osv(_('Error'), _('The Qty Consumed cant\'t be greater than the Indicative Stock'))
737
def change_expiry(self, cr, uid, id, expiry_date, product_id, location_id, uom, remark=False, context=None):
739
expiry date changes, find the corresponding internal prod lot
743
prodlot_obj = self.pool.get('stock.production.lot')
744
result = {'value':{}}
745
context.update({'location': location_id})
747
if expiry_date and product_id:
748
prod_ids = prodlot_obj.search(cr, uid, [('life_date', '=', expiry_date),
749
('type', '=', 'internal'),
750
('product_id', '=', product_id)], context=context)
753
result['warning'] = {'title': _('Error'),
754
'message': _('The selected Expiry Date does not exist in the system.')}
756
result['value'].update(expiry_date=False, prodlot_id=False)
758
# return first prodlot
759
result = self.change_prodlot(cr, uid, id, product_id, prod_ids[0], expiry_date, location_id, uom, context={})
760
result.setdefault('value',{}).update(prodlot_id=prod_ids[0])
761
if remark and remark in ('You must assign a batch number', 'You must assign an expiry date') :
762
result['value']['remark'] = ''
766
# clear expiry date, we clear production lot
767
result['value'].update(prodlot_id=False)
769
context.update(uom=uom)
770
context.update({'compute_child': False})
771
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
772
result['value'].update({'product_qty': product.qty_available})
776
def change_asset(self, cr, uid, id, asset, product_id, location_id, uom, remark=False, context=None):
778
Asset change, remove the remark
783
result = {'value':{}}
784
if remark and remark == 'You must assign an asset':
785
result.setdefault('value', {}).update(remark='')
790
def change_qty(self, cr, uid, ids, qty, product_id, prodlot_id, location, uom, context=None):
796
stock_qty = self._get_qty(cr, uid, product_id, prodlot_id, location, uom)
797
warn_msg = {'title': _('Error'), 'message': _("The Qty Consumed is greater than the Indicative Stock")}
800
res = self.pool.get('product.uom')._change_round_up_qty(cr, uid, uom, qty, 'consumed_qty', result=res)
802
if prodlot_id and qty > stock_qty:
803
res.setdefault('warning', {}).update(warn_msg)
804
res.setdefault('value', {}).update({'consumed_qty': 0})
806
res.setdefault('warning', {}).update(warn_msg)
810
def change_prodlot(self, cr, uid, ids, product_id, prodlot_id, expiry_date, location_id, uom, remark=False, context=None):
812
Set the expiry date according to the prodlot
817
context.update({'location': location_id, 'uom': uom})
818
if prodlot_id and not expiry_date:
819
if remark and remark in ('You must assign a batch number', 'You must assign an expiry date') :
820
res['value']['remark'] = ''
821
res['value'].update({'expiry_date': self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, context=context).life_date})
822
elif not prodlot_id and expiry_date:
823
res['value'].update({'expiry_date': False})
826
context.update({'compute_child': False})
827
product_qty = self.pool.get('product.product').browse(cr, uid, product_id, context=context).qty_available
829
if remark and remark in ('You must assign a batch number', 'You must assign an expiry date') :
830
res['value']['remark'] = ''
831
context.update({'location_id': location_id})
832
product_qty = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, context=context).stock_available
833
res['value'].update({'product_qty': product_qty})
837
def uom_onchange(self, cr, uid, ids, product_id, product_qty, location_id=False, uom=False, lot=False, context=None):
842
if uom and product_id:
843
qty_available = self._get_qty(cr, uid, product_id, lot, location_id, uom)
845
if not uom and product_id:
846
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
847
d['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)]
849
res = {'value': {'product_qty': qty_available}, 'domain': d}
852
res = self.pool.get('product.uom')._change_round_up_qty(cr, uid, uom, product_qty, 'consumed_qty', result=res)
856
def product_onchange(self, cr, uid, ids, product_id, location_id=False, uom=False, lot=False, context=None):
858
Set the product uom when the product change
862
product_obj = self.pool.get('product.product')
863
v = {'batch_mandatory': False, 'date_mandatory': False, 'asset_mandatory': False}
866
# Test the compatibility of the product with a consumption report
867
res, test = product_obj._on_change_restriction_error(cr, uid, product_id, field_name='product_id', values={'value': v}, vals={'constraints': 'consumption'}, context=context)
871
context.update({'location': location_id, 'uom': uom})
873
context.update({'compute_child': False})
874
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
875
qty_available = product.qty_available
877
if product.batch_management:
878
v.update({'batch_mandatory': True, 'remark': _('You must assign a batch')})
879
if product.perishable:
880
v.update({'date_mandatory': True, 'remark': _('You must assign an expiry date')})
881
if product.type == 'product' and product.subtype == 'asset':
882
v.update({'asset_mandatory': True, 'remark': _('You must assign an asset')})
884
uom = product.uom_id.id
885
v.update({'uom_id': uom})
886
d['uom_id'] = [('category_id', '=', product.uom_id.category_id.id)]
888
v.update({'product_qty': qty_available})
890
v.update({'uom_id': False, 'product_qty': 0.00, 'prodlot_id': False, 'expiry_date': False, 'consumed_qty': 0.00})
892
return {'value': v, 'domain': d}
894
def copy(self, cr, uid, line_id, default=None, context=None):
901
default.update({'prodlot_id': False, 'expiry_date': False, 'asset_id': False})
903
if 'consumed_qty' in default and default['consumed_qty'] < 0.00:
904
default['consumed_qty'] = 0.00
906
return super(real_average_consumption_line, self).copy(cr, uid, line_id[0], default=default, context={'noraise': True})
908
real_average_consumption_line()
911
class real_consumption_change_location(osv.osv_memory):
912
_name = 'real.consumption.change.location'
915
'report_id': fields.many2one('real.average.consumption', string='Report'),
916
'location_id': fields.many2one('stock.location', string='Consumer location', required=True),
919
def change_location(self, cr, uid, ids, context=None):
921
Change location of the report and reload the report
923
if isinstance(ids, (int, long)):
926
wiz = self.browse(cr, uid, ids[0], context=context)
928
self.pool.get('real.average.consumption').write(cr, uid, [wiz.report_id.id], {'cons_location_id': wiz.location_id.id}, context=context)
929
self.pool.get('real.average.consumption').button_update_stock(cr, uid, [wiz.report_id.id], context=context)
931
return {'type': 'ir.actions.act_window',
932
'res_model': 'real.average.consumption',
934
'view_mode': 'form,tree',
935
'res_id': wiz.report_id.id,
938
real_consumption_change_location()
941
class monthly_review_consumption(osv.osv):
942
_name = 'monthly.review.consumption'
943
_description = 'Monthly review consumption'
944
_rec_name = 'creation_date'
946
def _get_nb_lines(self, cr, uid, ids, field_name, args, context=None):
948
Returns the # of lines on the monthly review consumption
952
for mrc in self.browse(cr, uid, ids, context=context):
953
res[mrc.id] = len(mrc.line_ids)
957
def get_bool_values(self, cr, uid, ids, fields, arg, context=None):
959
if isinstance(ids, (int, long)):
961
for obj in self.browse(cr, uid, ids, context=context):
963
if any([item for item in obj.line_ids if item.to_correct_ok]):
968
'creation_date': fields.date(string='Creation date'),
969
'cons_location_id': fields.char(size=256, string='Location', readonly=True),
970
'period_from': fields.date(string='Period from', required=True),
971
'period_to': fields.date(string='Period to', required=True),
972
'sublist_id': fields.many2one('product.list', string='List/Sublist'),
973
'nomen_id': fields.many2one('product.nomenclature', string='Products\' nomenclature level'),
974
'line_ids': fields.one2many('monthly.review.consumption.line', 'mrc_id', string='Lines'),
975
'nb_lines': fields.function(_get_nb_lines, method=True, type='integer', string='# lines', readonly=True,),
976
'nomen_manda_0': fields.many2one('product.nomenclature', 'Main Type'),
977
'nomen_manda_1': fields.many2one('product.nomenclature', 'Group'),
978
'nomen_manda_2': fields.many2one('product.nomenclature', 'Family'),
979
'nomen_manda_3': fields.many2one('product.nomenclature', 'Root'),
980
'hide_column_error_ok': fields.function(get_bool_values, method=True, readonly=True, type="boolean", string="Show column errors", store=False),
984
'period_to': lambda *a: (DateFrom(time.strftime('%Y-%m-%d')) + RelativeDateTime(months=1, day=1, days=-1)).strftime('%Y-%m-%d'),
985
'creation_date': lambda *a: time.strftime('%Y-%m-%d'),
986
'cons_location_id': lambda *a: 'MSF Instance',
989
def period_change(self, cr, uid, ids, period_from, period_to, context=None):
991
Get the first day of month and the last day
996
res.update({'period_from': (DateFrom(period_from) + RelativeDateTime(day=1)).strftime('%Y-%m-%d')})
998
res.update({'period_to': (DateFrom(period_to) + RelativeDateTime(months=1, day=1, days=-1)).strftime('%Y-%m-%d')})
1000
return {'value': res}
1002
def import_fmc(self, cr, uid, ids, context=None):
1004
Launches the wizard to import lines from a file
1008
context.update({'active_id': ids[0]})
1010
return {'type': 'ir.actions.act_window',
1011
'res_model': 'wizard.import.fmc',
1012
'view_type': 'form',
1013
'view_mode': 'form',
1018
def export_fmc(self, cr, uid, ids, context=None):
1020
Return an xml file to open with Excel
1022
datas = {'ids': ids}
1024
return {'type': 'ir.actions.report.xml',
1025
'report_name': 'monthly.consumption.xls',
1028
# Creates a CSV file and launches the wizard to save it
1030
# if context is None:
1032
# fmc = self.browse(cr, uid, ids[0], context=context)
1034
# outfile = TemporaryFile('w+')
1035
# writer = csv.writer(outfile, quotechar='"', delimiter=',')
1036
# writer.writerow(['Product Code', 'Product Description', 'AMC', 'FMC', 'Valid until'])
1038
# for line in fmc.line_ids:
1039
# writer.writerow([line.name.default_code and line.name.default_code.encode('utf-8'), line.name.name and line.name.name.encode('utf-8'), line.amc, line.fmc, line.valid_until or ''])
1041
# file = base64.encodestring(outfile.read())
1044
# export_id = self.pool.get('wizard.export.fmc').create(cr, uid, {'fmc_id': ids[0], 'file': file,
1045
# 'filename': 'fmc_%s.csv' % (time.strftime('%Y_%m_%d')),
1046
# 'message': 'The FMC lines has been exported. Please click on Save As button to download the file'})
1048
# return {'type': 'ir.actions.act_window',
1049
# 'res_model': 'wizard.export.fmc',
1050
# 'res_id': export_id,
1051
# 'view_mode': 'form',
1052
# 'view_type': 'form',
1056
def fill_lines(self, cr, uid, ids, context=None):
1058
Fill all lines according to defined nomenclature level and sublist
1062
line_obj = self.pool.get('monthly.review.consumption.line')
1063
for report in self.browse(cr, uid, ids, context=context):
1066
# Get all products for the defined nomenclature
1069
if report.nomen_manda_3:
1070
nom = report.nomen_manda_3.id
1071
field = 'nomen_manda_3'
1072
elif report.nomen_manda_2:
1073
nom = report.nomen_manda_2.id
1074
field = 'nomen_manda_2'
1075
elif report.nomen_manda_1:
1076
nom = report.nomen_manda_1.id
1077
field = 'nomen_manda_1'
1078
elif report.nomen_manda_0:
1079
nom = report.nomen_manda_0.id
1080
field = 'nomen_manda_0'
1082
product_ids.extend(self.pool.get('product.product').search(cr, uid, [(field, '=', nom)], context=context))
1084
# Get all products for the defined list
1085
if report.sublist_id:
1086
for line in report.sublist_id.product_ids:
1087
product_ids.append(line.name.id)
1089
# Check if products in already existing lines are in domain
1091
for line in report.line_ids:
1092
if line.name.id in product_ids:
1093
products.append(line.name.id)
1095
self.pool.get('monthly.review.consumption.line').unlink(cr, uid, line.id, context=context)
1097
amc_context = context.copy()
1098
amc_context.update({'from_date': report.period_from, 'to_date': report.period_to})
1099
if amc_context.get('from_date', False):
1100
from_date = (DateFrom(amc_context.get('from_date')) + RelativeDateTime(day=1)).strftime('%Y-%m-%d')
1101
amc_context.update({'from_date': from_date})
1103
if amc_context.get('to_date', False):
1104
to_date = (DateFrom(amc_context.get('to_date')) + RelativeDateTime(months=1, day=1, days=-1)).strftime('%Y-%m-%d')
1105
amc_context.update({'to_date': to_date})
1107
for product in self.pool.get('product.product').browse(cr, uid, product_ids, context=context):
1108
# Check if the product is not already on the report
1109
if product.id not in products:
1110
products.append(product.id)
1111
amc = self.pool.get('product.product').compute_amc(cr, uid, product.id, context=amc_context)
1112
last_fmc_reviewed = False
1113
line_ids = line_obj.search(cr, uid, [('name', '=', product.id), ('valid_ok', '=', True)], order='valid_until desc, id desc', context=context)
1115
for line in line_obj.browse(cr, uid, [line_ids[0]], context=context):
1116
last_fmc_reviewed = line.mrc_id.creation_date
1117
self.pool.get('monthly.review.consumption.line').create(cr, uid, {'name': product.id,
1121
'last_reviewed': last_fmc_reviewed,
1122
'last_reviewed2': last_fmc_reviewed,
1123
'mrc_id': report.id})
1125
return {'type': 'ir.actions.act_window',
1126
'res_model': 'monthly.review.consumption',
1127
'view_type': 'form',
1128
'view_mode': 'form',
1134
def valid_multiple_lines(self, cr, uid, ids, context=None):
1136
Validate multiple lines
1140
self.check_lines_to_fix(cr, uid, ids, context)
1141
for report in self.browse(cr, uid, ids, context=context):
1142
for line in report.line_ids:
1143
if not line.valid_ok:
1144
self.pool.get('monthly.review.consumption.line').valid_line(cr, uid, line.id, context=context)
1146
return {'type': 'ir.actions.act_window',
1147
'res_model': 'monthly.review.consumption',
1148
'view_type': 'form',
1149
'view_mode': 'form',
1154
def write(self, cr, uid, ids, vals, context=None):
1155
if vals.get('sublist_id',False):
1156
vals.update({'nomen_manda_0':False,'nomen_manda_1':False,'nomen_manda_2':False,'nomen_manda_3':False})
1157
if vals.get('nomen_manda_0',False):
1158
vals.update({'sublist_id':False})
1159
ret = super(monthly_review_consumption, self).write(cr, uid, ids, vals, context=context)
1162
def onChangeSearchNomenclature(self, cr, uid, id, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=True, context=None):
1163
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})
1165
def get_nomen(self, cr, uid, id, field):
1166
return self.pool.get('product.nomenclature').get_nomen(cr, uid, self, id, field, context={'withnum': 1})
1168
def button_remove_lines(self, cr, uid, ids, context=None):
1174
if isinstance(ids, (int, long)):
1177
vals['line_ids'] = []
1178
for line in self.browse(cr, uid, ids, context=context):
1179
line_browse_list = line.line_ids
1180
for var in line_browse_list:
1181
vals['line_ids'].append((2, var.id))
1182
self.write(cr, uid, ids, vals, context=context)
1185
def check_lines_to_fix(self, cr, uid, ids, context=None):
1187
Check the lines that need to be corrected.
1189
if isinstance(ids, (int, long)):
1191
for var in self.browse(cr, uid, ids, context=context):
1192
# we check the lines that need to be fixed
1194
for var in var.line_ids:
1195
if var.to_correct_ok:
1196
raise osv.except_osv(_('Warning !'), _('Some lines need to be fixed before.'))
1199
monthly_review_consumption()
1202
class monthly_review_consumption_line(osv.osv):
1203
_name = 'monthly.review.consumption.line'
1204
_description = 'Monthly review consumption line'
1207
def _get_amc(self, cr, uid, ids, field_name, arg, ctx=None):
1209
Calculate the product AMC for the period
1213
context = ctx.copy()
1216
for line in self.browse(cr, uid, ids, context=context):
1217
context.update({'from_date': line.mrc_id.period_from, 'to_date': line.mrc_id.period_to})
1218
if context.get('from_date', False):
1219
from_date = (DateFrom(context.get('from_date')) + RelativeDateTime(day=1)).strftime('%Y-%m-%d')
1220
context.update({'from_date': from_date})
1222
if context.get('to_date', False):
1223
to_date = (DateFrom(context.get('to_date')) + RelativeDateTime(months=1, day=1, days=-1)).strftime('%Y-%m-%d')
1224
context.update({'to_date': to_date})
1226
res[line.id] = self.pool.get('product.product').compute_amc(cr, uid, line.name.id, context=context)
1230
def _get_last_fmc(self, cr, uid, ids, field_name, args, context=None):
1232
Returns the last fmc date
1238
for line in self.browse(cr, uid, ids, context=context):
1239
res[line.id] = self.product_onchange(cr, uid, line.id, line.name.id, line.mrc_id.id, context=context).get('value', {}).get('last_reviewed', None)
1243
def create(self, cr, uid, vals, context=None):
1247
vals.update({'fmc': vals.get('fmc2')})
1248
if 'last_reviewed2' in vals:
1249
vals.update({'last_reviewed': vals.get('last_reviewed2')})
1251
if vals.get('valid_ok') and not vals.get('last_reviewed'):
1252
vals.update({'last_reviewed': time.strftime('%Y-%m-%d'),
1253
'last_reviewed2': time.strftime('%Y-%m-%d')})
1255
return super(monthly_review_consumption_line, self).create(cr, uid, vals, context=context)
1257
def write(self, cr, uid, ids, vals, context=None):
1260
if isinstance(ids, (int, long)):
1262
if not context.get('import_in_progress') and not context.get('button'):
1263
obj_data = self.pool.get('ir.model.data')
1264
tbd_product = obj_data.get_object_reference(cr, uid, 'msf_doc_import', 'product_tbd')[1]
1265
if vals.get('name'):
1266
if vals.get('name') == tbd_product:
1267
raise osv.except_osv(_('Warning !'), _('You have to define a valid product, i.e. not "To be define".'))
1269
vals['to_correct_ok'] = False
1270
vals['text_error'] = False
1272
vals.update({'fmc': vals.get('fmc2')})
1273
if 'last_reviewed2' in vals:
1274
vals.update({'last_reviewed': vals.get('last_reviewed2')})
1276
if vals.get('valid_ok') and not vals.get('last_reviewed'):
1277
vals.update({'last_reviewed': time.strftime('%Y-%m-%d'),
1278
'last_reviewed2': time.strftime('%Y-%m-%d')})
1280
return super(monthly_review_consumption_line, self).write(cr, uid, ids, vals, context=context)
1282
def _get_mrc_change(self, cr, uid, ids, context=None):
1284
Returns MRC ids when Date change
1287
for mrc in self.pool.get('monthly.review.consumption').browse(cr, uid, ids, context=context):
1288
for line in mrc.line_ids:
1289
result[line.id] = True
1291
return result.keys()
1293
def _get_product(self, cr, uid, ids, context=None):
1294
return self.pool.get('monthly.review.consumption.line').search(cr, uid, [('name', 'in', ids)], context=context)
1296
def _get_checks_all(self, cr, uid, ids, name, arg, context=None):
1299
result[id] = {'to_correct_ok': False}
1301
for out in self.browse(cr, uid, ids, context=context):
1302
# the lines with to_correct_ok=True will be red
1304
result[out.id]['to_correct_ok'] = True
1308
'name': fields.many2one('product.product', string='Product', required=True),
1309
'ref': fields.related('name', 'default_code', type='char', size=64, readonly=True,
1310
store={'product.product': (_get_product, ['default_code'], 10),
1311
'monthly.review.consumption.line': (lambda self, cr, uid, ids, c=None: ids, ['name'], 20)}),
1312
'amc': fields.function(_get_amc, string='AMC', method=True, readonly=True,
1313
store={'monthly.review.consumption': (_get_mrc_change, ['period_from', 'period_to'], 20),
1314
'monthly.review.consumption.line': (lambda self, cr, uid, ids, c=None: ids, [],20),}),
1315
'fmc': fields.float(digits=(16,2), string='FMC'),
1316
'fmc2': fields.float(digits=(16,2), string='FMC (hidden)'),
1317
#'last_reviewed': fields.function(_get_last_fmc, method=True, type='date', string='Last reviewed on', readonly=True, store=True),
1318
'last_reviewed': fields.date(string='Last reviewed on', readonly=True),
1319
'last_reviewed2': fields.date(string='Last reviewed on (hidden)'),
1320
'valid_until': fields.date(string='Valid until'),
1321
'valid_ok': fields.boolean(string='Validated', readonly=False),
1322
'mrc_id': fields.many2one('monthly.review.consumption', string='MRC', required=True, ondelete='cascade'),
1323
'mrc_creation_date': fields.related('mrc_id', 'creation_date', type='date', store=True),
1324
'text_error': fields.text('Errors', readonly=True),
1325
'to_correct_ok': fields.function(_get_checks_all, method=True, type="boolean", string="To correct", store=False, readonly=True, multi="m"),
1328
def valid_line(self, cr, uid, ids, context=None):
1330
Valid the line and enter data in product form
1335
if isinstance(ids, (int, long)):
1338
for line in self.browse(cr, uid, ids, context=context):
1340
raise osv.except_osv(_('Error'), _('The line is already validated !'))
1342
self.write(cr, uid, [line.id], {'valid_ok': True,
1343
'last_reviewed': time.strftime('%Y-%m-%d'),
1344
'last_reviewed2': time.strftime('%Y-%m-%d')}, context=context)
1348
def display_graph(self, cr, uid, ids, context=None):
1350
Display the graph view of the line
1352
raise osv.except_osv('Error !', 'Not implemented yet !')
1354
def fmc_change(self, cr, uid, ids, amc, fmc, product_id, context=None):
1356
Valid the line if the FMC is manually changed
1363
res.update({'valid_ok': True, 'last_reviewed': time.strftime('%Y-%m-%d'), 'fmc2': fmc, 'last_reviewed2': time.strftime('%Y-%m-%d')})
1365
last_fmc_reviewed = False
1366
domain = [('name', '=', product_id), ('valid_ok', '=', True)]
1367
line_ids = self.search(cr, uid, domain, order='valid_until desc, mrc_creation_date desc', context=context)
1370
for line in self.browse(cr, uid, [line_ids[0]], context=context):
1371
last_fmc_reviewed = line.mrc_id.creation_date
1373
res.update({'last_reviewed': last_fmc_reviewed, 'last_reviewed2': last_fmc_reviewed, 'fmc2': fmc})
1375
return {'value': res}
1377
def product_onchange(self, cr, uid, ids, product_id, mrc_id=False, from_date=False, to_date=False, context=None):
1379
Fill data in the line
1381
if isinstance(ids, (int, long)):
1387
# Compute the AMC on the period of the consumption report
1388
context.update({'from_date': from_date, 'to_date': to_date})
1390
product_obj = self.pool.get('product.product')
1391
line_obj = self.pool.get('monthly.review.consumption.line')
1393
last_fmc_reviewed = False
1396
return {'value': {'amc': 0.00,
1399
'last_reviewed2': 0.00,
1400
'last_reviewed': None,
1401
'valid_until': False,
1404
domain = [('name', '=', product_id), ('valid_ok', '=', True)]
1406
line_ids = line_obj.search(cr, uid, domain, order='valid_until desc, mrc_creation_date desc', context=context)
1409
for line in self.browse(cr, uid, [line_ids[0]], context=context):
1410
last_fmc_reviewed = line.mrc_id.creation_date
1412
if context.get('from_date', False):
1413
from_date = (DateFrom(context.get('from_date')) + RelativeDateTime(day=1)).strftime('%Y-%m-%d')
1414
context.update({'from_date': from_date})
1416
if context.get('to_date', False):
1417
to_date = (DateFrom(context.get('to_date')) + RelativeDateTime(months=1, day=1, days=-1)).strftime('%Y-%m-%d')
1418
context.update({'to_date': to_date})
1420
amc = product_obj.compute_amc(cr, uid, product_id, context=context)
1421
return {'value': {'amc': amc,
1424
'last_reviewed': last_fmc_reviewed,
1425
'last_reviewed2': last_fmc_reviewed,
1426
'valid_until': False,
1430
monthly_review_consumption_line()
1433
class product_product(osv.osv):
1434
_name = 'product.product'
1435
_inherit = 'product.product'
1437
def _compute_fmc(self, cr, uid, ids, field_name, args, context=None):
1439
Returns the last value of the FMC
1445
#fmc_obj = self.pool.get('monthly.review.consumption')
1446
fmc_line_obj = self.pool.get('monthly.review.consumption.line')
1448
# Search all Review report for locations
1449
#fmc_ids = fmc_obj.search(cr, uid, [], order='period_to desc, creation_date desc', limit=1, context=context)
1454
# Search all validated lines with the product
1455
#line_ids = fmc_line_obj.search(cr, uid, [('name', '=', product), ('valid_ok', '=', True), ('mrc_id', 'in', fmc_ids)], context=context)
1456
line_ids = fmc_line_obj.search(cr, uid, [('name', '=', product), ('valid_ok', '=', True)], order='last_reviewed desc, mrc_id desc', limit=1, context=context)
1458
# Get the last created line
1459
for line in fmc_line_obj.browse(cr, uid, line_ids, context=context):
1460
res[product] = line.fmc
1464
def compute_mac(self, cr, uid, ids, field_name, args, context=None):
1466
Compute the Real Average Consumption
1468
if isinstance(ids, (int, long)):
1473
uom_obj = self.pool.get('product.uom')
1475
rac_domain = [('created_ok', '=', True)]
1483
# Read if a interval is defined
1484
if context.get('from_date', False):
1485
from_date = context.get('from_date')
1486
rac_domain.append(('period_to', '>=', from_date))
1488
if context.get('to_date', False):
1489
to_date = context.get('to_date')
1490
rac_domain.append(('period_to', '<=', to_date))
1492
# Filter for one or some locations
1493
if context.get('location_id', False):
1494
if type(context['location_id']) == type(1):
1495
location_ids = [context['location_id']]
1496
elif type(context['location_id']) in (type(''), type(u'')):
1497
location_ids = self.pool.get('stock.location').search(cr, uid, [('name','ilike',context['location'])], context=context)
1499
location_ids = context.get('location_id', [])
1503
if from_date and to_date:
1504
rcr_domain = ['&', '&', ('product_id', 'in', ids), ('rac_id.cons_location_id', 'in', location_ids),
1505
# All lines with a report started out the period and finished in the period
1506
'|', '&', ('rac_id.period_to', '>=', from_date), ('rac_id.period_to', '<=', to_date),
1507
# All lines with a report started in the period and finished out the period
1508
'|', '&', ('rac_id.period_from', '<=', to_date), ('rac_id.period_from', '>=', from_date),
1509
# All lines with a report started before the period and finished after the period
1510
'&', ('rac_id.period_from', '<=', from_date), ('rac_id.period_to', '>=', to_date)]
1512
rcr_line_ids = self.pool.get('real.average.consumption.line').search(cr, uid, rcr_domain, context=context)
1513
for line in self.pool.get('real.average.consumption.line').browse(cr, uid, rcr_line_ids, context=context):
1514
cons = self._get_period_consumption(cr, uid, line, from_date, to_date, context=context)
1515
res[id] += uom_obj._compute_qty(cr, uid, line.uom_id.id, cons, line.product_id.uom_id.id)
1517
# We want the average for the entire period
1518
if to_date < from_date:
1519
raise osv.except_osv(_('Error'), _('You cannot have a \'To Date\' younger than \'From Date\'.'))
1520
# Calculate the # of months in the period
1522
to_date_str = strptime(to_date, '%Y-%m-%d')
1524
to_date_str = strptime(to_date, '%Y-%m-%d %H:%M:%S')
1527
from_date_str = strptime(from_date, '%Y-%m-%d')
1529
from_date_str = strptime(from_date, '%Y-%m-%d %H:%M:%S')
1531
nb_months = self._get_date_diff(from_date_str, to_date_str)
1533
if not nb_months: nb_months = 1
1535
uom_id = self.browse(cr, uid, ids[0], context=context).uom_id.id
1536
res[id] = res[id]/nb_months
1537
res[id] = round(self.pool.get('product.uom')._compute_qty(cr, uid, uom_id, res[id], uom_id), 2)
1541
def compute_amc(self, cr, uid, ids, context=None):
1543
Compute the Average Monthly Consumption with this formula :
1544
AMC = (sum(OUTGOING (except reason types Loan, Donation, Loss, Discrepancy))
1546
sum(INCOMING with reason type Return from unit)) / Number of period's months
1547
The AMC is the addition of all done stock moves for a product within a period.
1548
For stock moves generated from a real consumption report, the qty of product is computed
1549
according to the average of consumption for the time of the period.
1554
if isinstance(ids, (int, long)):
1557
move_obj = self.pool.get('stock.move')
1558
uom_obj = self.pool.get('product.uom')
1565
# Read if a interval is defined
1566
if context.get('from_date', False):
1567
from_date = context.get('from_date')
1569
if context.get('to_date', False):
1570
to_date = context.get('to_date')
1572
# Get all reason types
1573
loan_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_loan')[1]
1574
donation_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_donation')[1]
1575
donation_exp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_donation_expiry')[1]
1576
loss_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_loss')[1]
1577
discrepancy_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_discrepancy')[1]
1578
return_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_return_from_unit')[1]
1579
return_good_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_goods_return')[1]
1580
replacement_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_goods_replacement')[1]
1583
domain = [('state', '=', 'done'), ('reason_type_id', 'not in', (loan_id, donation_id, donation_exp_id, loss_id, discrepancy_id)), ('product_id', 'in', ids)]
1585
domain.append(('date', '<=', to_date))
1587
domain.append(('date', '>=', from_date))
1589
locations = self.pool.get('stock.location').search(cr, uid, [('usage', 'in', ('internal', 'customer'))], context=context)
1590
# Add locations filters in domain if locations are passed in context
1591
domain.append(('location_id', 'in', locations))
1592
domain.append(('location_dest_id', 'in', locations))
1594
# Search all real consumption line included in the period
1595
# If no period found, take all stock moves
1596
if from_date and to_date:
1597
rcr_domain = ['&', ('product_id', 'in', ids),
1598
# All lines with a report started out the period and finished in the period
1599
'|', '&', ('rac_id.period_to', '>=', from_date), ('rac_id.period_to', '<=', to_date),
1600
# All lines with a report started in the period and finished out the period
1601
'|', '&', ('rac_id.period_from', '<=', to_date), ('rac_id.period_from', '>=', from_date),
1602
# All lines with a report started before the period and finished after the period
1603
'&', ('rac_id.period_from', '<=', from_date), ('rac_id.period_to', '>=', to_date)]
1605
rcr_line_ids = self.pool.get('real.average.consumption.line').search(cr, uid, rcr_domain, context=context)
1606
report_move_ids = []
1607
for line in self.pool.get('real.average.consumption.line').browse(cr, uid, rcr_line_ids, context=context):
1608
report_move_ids.append(line.move_id.id)
1609
res += self._get_period_consumption(cr, uid, line, from_date, to_date, context=context)
1612
domain.append(('id', 'not in', report_move_ids))
1614
out_move_ids = move_obj.search(cr, uid, domain, context=context)
1616
for move in move_obj.browse(cr, uid, out_move_ids, context=context):
1617
if move.reason_type_id.id in (return_id, return_good_id, replacement_id) and move.location_id.usage == 'customer':
1618
res -= uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, move.product_id.uom_id.id)
1619
elif move.location_dest_id.usage == 'customer':
1620
res += uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, move.product_id.uom_id.id)
1622
# Update the limit in time
1623
if not context.get('from_date') and (not from_date or move.date < from_date):
1624
from_date = move.date
1625
if not context.get('to_date') and (not to_date or move.date > to_date):
1628
if not to_date or not from_date:
1631
# We want the average for the entire period
1632
if to_date < from_date:
1633
raise osv.except_osv(_('Error'), _('You cannot have a \'To Date\' younger than \'From Date\'.'))
1634
# Calculate the # of months in the period
1636
to_date_str = strptime(to_date, '%Y-%m-%d')
1638
to_date_str = strptime(to_date, '%Y-%m-%d %H:%M:%S')
1641
from_date_str = strptime(from_date, '%Y-%m-%d')
1643
from_date_str = strptime(from_date, '%Y-%m-%d %H:%M:%S')
1645
nb_months = self._get_date_diff(from_date_str, to_date_str)
1647
if not nb_months: nb_months = 1
1649
uom_id = self.read(cr, uid, ids[0], ['uom_id'], context=context)['uom_id'][0]
1651
res = self.pool.get('product.uom')._compute_qty(cr, uid, uom_id, res, uom_id)
1655
def _get_date_diff(self, from_date, to_date):
1657
Returns the number of months between to dates according to the number
1658
of days in the month.
1660
diff_date = Age(to_date, from_date)
1663
def days_in_month(month, year):
1665
Returns the # of days in the month
1668
if month == 2 and year%4 == 0:
1670
elif month == 2 and year%4 != 0:
1672
elif month in (1, 3, 5, 7, 8, 10, 12):
1676
while from_date <= to_date:
1677
# Add 12 months by years between the two dates
1679
res += diff_date.years*12
1680
from_date += RelativeDate(years=diff_date.years)
1681
diff_date = Age(to_date, from_date)
1683
# If two dates are in the same month
1684
if from_date.month == to_date.month:
1685
nb_days_in_month = days_in_month(from_date.month, from_date.year)
1686
# We divided the # of days between the two dates by the # of days in month
1687
# to have a percentage of the number of month
1688
res += round((to_date.day-from_date.day+1)/nb_days_in_month, 2)
1690
elif to_date.month - from_date.month > 1 or to_date.year - from_date.year > 0:
1692
from_date += RelativeDate(months=1)
1694
# Number of month till the end of from month
1695
fr_nb_days_in_month = days_in_month(from_date.month, from_date.year)
1696
nb_days = fr_nb_days_in_month - from_date.day + 1
1697
res += round(nb_days/fr_nb_days_in_month, 2)
1698
# Number of month till the end of from month
1699
to_nb_days_in_month = days_in_month(to_date.month, to_date.year)
1700
res += round(to_date.day/to_nb_days_in_month, 2)
1707
def _compute_product_amc(self, cr, uid, ids, field_name, args, ctx=None):
1710
context = ctx.copy()
1712
from_date = (DateFrom(time.strftime('%Y-%m-%d')) + RelativeDateTime(months=-3, day=1)).strftime('%Y-%m-%d')
1713
to_date = (DateFrom(time.strftime('%Y-%m-%d')) + RelativeDateTime(day=1, days=-1)).strftime('%Y-%m-%d')
1715
if context.get('from_date', False):
1716
from_date = (DateFrom(context.get('from_date')) + RelativeDateTime(day=1)).strftime('%Y-%m-%d')
1718
if context.get('to_date', False):
1719
to_date = (DateFrom(context.get('to_date')) + RelativeDateTime(months=1, day=1, days=-1)).strftime('%Y-%m-%d')
1721
context.update({'from_date': from_date})
1722
context.update({'to_date': to_date})
1725
res[product] = self.compute_amc(cr, uid, product, context=context)
1729
def _get_period_consumption(self, cr, uid, line, from_date, to_date, context=None):
1731
Returns the average quantity of product in the period
1733
# Compute the # of days in the report period
1736
from datetime import datetime
1737
report_from = datetime.strptime(line.rac_id.period_from, '%Y-%m-%d')
1738
report_to = datetime.strptime(line.rac_id.period_to, '%Y-%m-%d')
1739
dt_from_date = datetime.strptime(from_date, '%Y-%m-%d')
1740
dt_to_date = datetime.strptime(to_date, '%Y-%m-%d')
1741
delta = report_to - report_from
1743
# Add 1 to include the last day of report to
1744
report_nb_days = delta.days + 1
1747
# Case where the report is totally included in the period
1748
if line.rac_id.period_from >= from_date and line.rac_id.period_to <= to_date:
1749
return line.consumed_qty
1750
# Case where the report started before the period and done after the period
1751
elif line.rac_id.period_from <= from_date and line.rac_id.period_to >= to_date:
1752
# Compute the # of days of the period
1753
delta2 = dt_to_date - dt_from_date
1754
days_incl = delta2.days +1
1755
# Case where the report started before the period and done in the period
1756
elif line.rac_id.period_from <= from_date and line.rac_id.period_to <= to_date and line.rac_id.period_to >= from_date:
1757
# Compute the # of days of the report included in the period
1758
# Add 1 to include the last day of report to
1759
delta2 = report_to - dt_from_date
1760
days_incl = delta2.days +1
1761
# Case where the report started in the period and done after the period
1762
elif line.rac_id.period_from >= from_date and line.rac_id.period_to >= to_date and line.rac_id.period_from <= to_date:
1763
# Compute the # of days of the report included in the period
1764
# Add 1 to include the last day of to_date
1765
delta2 = dt_to_date - report_from
1766
days_incl = delta2.days +1
1768
# Compute the quantity consumed in the period for this line
1769
consumed_qty = (line.consumed_qty/report_nb_days)*days_incl
1770
return self.pool.get('product.uom')._compute_qty(cr, uid, line.uom_id.id, consumed_qty, line.uom_id.id)
1773
'procure_delay': fields.float(digits=(16,2), string='Procurement Lead Time',
1774
help='It\'s the default time to procure this product. This lead time will be used on the Order cycle procurement computation'),
1775
'monthly_consumption': fields.function(compute_mac, method=True, type='float', string='Real Consumption', readonly=True),
1776
'product_amc': fields.function(_compute_product_amc, method=True, type='float', string='Monthly consumption', readonly=True),
1777
'reviewed_consumption': fields.function(_compute_fmc, method=True, type='float', string='Forecasted Monthly Consumption', readonly=True),
1781
'procure_delay': lambda *a: 60,
1788
class stock_picking(osv.osv):
1789
_inherit = 'stock.picking'
1790
_name = 'stock.picking'
1792
def _hook_log_picking_modify_message(self, cr, uid, ids, context=None, message='', pick=False):
1794
Possibility to change the message
1796
report_ids = self.pool.get('real.average.consumption').search(cr, uid, [('picking_id', '=', pick.id)], context=context)
1798
name = self.pool.get('real.average.consumption').browse(cr, uid, report_ids[0], context=context).picking_id.name
1799
return 'Delivery Order %s generated from the consumption report is closed.' % name
1801
return super(stock_picking, self)._hook_log_picking_modify_message(cr, uid, ids, context=context, message=message, pick=pick)
1805
class stock_location(osv.osv):
1806
_inherit = 'stock.location'
1808
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1811
if context.get('no3buttons') and view_type == 'search':
1812
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'consumption_calculation', 'view_stock_location_without_buttons')
1813
return super(stock_location, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)