1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 MSF, TeMPO Consulting
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
##############################################################################
22
from osv import osv, fields
23
from tools.translate import _
25
from datetime import datetime, timedelta
26
from dateutil.relativedelta import relativedelta
27
import decimal_precision as dp
33
KIT_COMPOSITION_TYPE = [('theoretical', 'Theoretical'),
37
KIT_STATE = [('draft', 'Draft'),
38
('in_production', 'In Production'),
39
('completed', 'Completed'),
43
class composition_kit(osv.osv):
45
kit composition class, representing both theoretical composition and actual ones
47
_name = 'composition.kit'
49
def get_default_expiry_date(self, cr, uid, ids, context=None):
51
default value for kits
55
def _compute_expiry_date(self, cr, uid, ids, context=None):
57
compute the expiry date of real composition.kit based on items
60
date_obj = self.pool.get('date.tools')
61
db_date_format = date_obj.get_db_date_format(cr, uid, context=context)
62
date_format = date_obj.get_date_format(cr, uid, context=context)
64
for obj in self.browse(cr, uid, ids, context=context):
65
# if no expiry date from items (no perishable products or no expiry date entered), the default value is '9999-01-01'
67
# computation of expiry date makes sense only for real type
68
if obj.composition_type != 'real':
69
raise osv.except_osv(_('Warning !'), _('Computation of expiry date is only available for Composition List.'))
70
for item in obj.composition_item_ids:
72
if not expiry_date or datetime.strptime(item.item_exp, db_date_format) < datetime.strptime(expiry_date, db_date_format):
73
expiry_date = item.item_exp
75
expiry_date = self.get_default_expiry_date(cr, uid, ids, context=context)
79
def modify_expiry_date(self, cr, uid, ids, context=None):
81
open modify expiry date wizard
87
name = _("Modify Expiry Date")
88
model = 'modify.expiry.date'
90
wiz_obj = self.pool.get('wizard')
92
data = self.read(cr, uid, ids, ['composition_exp'], context=context)[0]
93
date = data['composition_exp']
94
# open the selected wizard
95
res = wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=dict(context,
100
def mark_as_completed(self, cr, uid, ids, context=None):
103
set the state to 'completed'
108
if isinstance(ids, (int, long)):
111
for obj in self.browse(cr, uid, ids, context=context):
112
if not len(obj.composition_item_ids):
113
raise osv.except_osv(_('Warning !'), _('Kit Composition cannot be empty.'))
115
raise osv.except_osv(_('Warning !'), _('Cannot complete inactive kit.'))
116
for item in obj.composition_item_ids:
117
if item.item_qty <= 0:
118
raise osv.except_osv(_('Warning !'), _('Kit Items must have a quantity greater than 0.0.'))
119
self.write(cr, uid, ids, {'state': 'completed'}, context=context)
122
def mark_as_inactive(self, cr, uid, ids, context=None):
125
set the active flag to False
130
if isinstance(ids, (int, long)):
133
for obj in self.browse(cr, uid, ids, context=context):
134
if obj.composition_type != 'theoretical':
135
raise osv.except_osv(_('Warning !'), _('Only theoretical kit can manipulate "active" field.'))
136
self.write(cr, uid, ids, {'active': False}, context=context)
139
def mark_as_active(self, cr, uid, ids, context=None):
142
set the active flag to False
147
if isinstance(ids, (int, long)):
150
for obj in self.browse(cr, uid, ids, context=context):
151
if obj.composition_type != 'theoretical':
152
raise osv.except_osv(_('Warning !'), _('Only theoretical kit can manipulate "active" field.'))
153
self.write(cr, uid, ids, {'active': True}, context=context)
156
def close_kit(self, cr, uid, ids, context=None):
159
set the state to 'done'
164
if isinstance(ids, (int, long)):
167
self.write(cr, uid, ids, {'state': 'done'}, context=context)
170
def reset_to_version(self, cr, uid, ids, context=None):
172
open confirmation wizard
175
name = _("Reset Items to Version Reference. Are you sure?")
178
question = 'The item list of current composition list will be reset to reference list from the selected Version. Are you sure ?'
179
clazz = 'composition.kit'
180
func = 'do_reset_to_version'
183
# to reset to version
184
for obj in self.browse(cr, uid, ids, context=context):
186
if obj.composition_type != 'real':
187
raise osv.except_osv(_('Warning !'), _('Only composition lists can be reset to a version.'))
188
# a version must have been selected
189
if not obj.composition_version_id:
190
raise osv.except_osv(_('Warning !'), _('The composition list is not linked to any version.'))
192
wiz_obj = self.pool.get('wizard')
193
# open the selected wizard
194
res = wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=dict(context, question=question,
195
callback={'clazz': clazz,
201
def do_reset_to_version(self, cr, uid, ids, context=None):
203
remove all items and create one item for each item from the referenced version
206
item_obj = self.pool.get('composition.item')
207
# unlink all composition items corresponding to selected kits
208
item_ids = item_obj.search(cr, uid, [('item_kit_id', 'in', ids)], context=context)
209
item_obj.unlink(cr, uid, item_ids, context=context)
210
for obj in self.browse(cr, uid, ids, context=context):
211
# copy all items from the version
212
for item_v in obj.composition_version_id.composition_item_ids:
213
values = {'item_module': item_v.item_module,
214
'item_product_id': item_v.item_product_id.id,
215
'item_qty': item_v.item_qty,
216
'item_uom_id': item_v.item_uom_id.id,
217
'item_lot': item_v.item_lot,
218
'item_exp': item_v.item_exp,
219
'item_kit_id': obj.id,
220
'item_description': item_v.item_description,
222
item_obj.create(cr, uid, values, context=context)
223
# we display the composition list view form
224
return {'name':_("Kit Composition List"),
225
'view_mode': 'form,tree',
227
'res_model': 'composition.kit',
229
'type': 'ir.actions.act_window',
231
'domain': [('composition_type', '=', 'real')],
232
'context': {'composition_type':'real'},
235
def _generate_item_mirror_objects(self, cr, uid, ids, wizard_data, context=None):
237
Generate memory objects as mirror for kit items, which can be modified (batch number policy needs modification,
238
we therefore cannot link items directly).
243
mirror_obj = self.pool.get('substitute.item.mirror')
244
# returned list, list of created ids
246
for obj in self.browse(cr, uid, ids, context=context):
247
for item in obj.composition_item_ids:
248
# create a mirror object which can be later selected and modified in the many2many field
249
batch_management = item.item_product_id.batch_management
250
perishable = item.item_product_id.perishable
251
values = {'wizard_id': wizard_data['res_id'],
252
'item_id_mirror': item.id,
253
'kit_id_mirror': item.item_kit_id.id,
254
'module_substitute_item': item.item_module,
255
'product_id_substitute_item': item.item_product_id.id,
256
'qty_substitute_item': item.item_qty,
257
'uom_id_substitute_item': item.item_uom_id.id,
258
'lot_mirror': item.item_lot,
259
'exp_substitute_item': item.item_exp,
260
'hidden_batch_management_mandatory': batch_management,
261
'hidden_perishable_mandatory': perishable,
263
id = mirror_obj.create(cr, uid, values, context=context)
267
def substitute_items(self, cr, uid, ids, context=None):
269
substitute lines from the composition kit with created new lines
271
# we need the context for the wizard switch
275
name = _("Substitute Kit Items")
278
wiz_obj = self.pool.get('wizard')
279
# open the selected wizard
280
res = wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=dict(context, kit_id=ids[0]))
281
# write wizard id back in the wizard object, cannot use ID in the wizard form... openERP bug ?
282
self.pool.get(model).write(cr, uid, [res['res_id']], {'wizard_id': res['res_id']}, context=res['context'])
283
# generate mirrors item objects
284
self._generate_item_mirror_objects(cr, uid, ids, wizard_data=res, context=res['context'])
287
def do_substitute(self, cr, uid, ids, context=None):
289
call the modify expiry date window for possible modification of expiry date
291
res = self.modify_expiry_date(cr, uid, ids, context=context)
294
def de_kitting(self, cr, uid, ids, context=None):
296
explode the kit, preselecting all mirror items
298
# we need the context for the wizard switch
302
name = _("De-Kitting")
305
wiz_obj = self.pool.get('wizard')
306
# open the selected wizard
307
res = wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=dict(context, kit_id=ids[0]))
308
# write wizard id back in the wizard object, cannot use ID in the wizard form... openERP bug ?
309
self.pool.get(model).write(cr, uid, [res['res_id']], {'wizard_id': res['res_id']}, context=context)
310
# generate mirrors item objects
311
data = self._generate_item_mirror_objects(cr, uid, ids, wizard_data=res, context=context)
312
# fill all elements into the many2many field
313
self.pool.get(model).write(cr, uid, [res['res_id']], {'composition_item_ids': [(6,0,data)]}, context=context)
316
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
318
multi fields function method
323
if isinstance(ids, (int, long)):
328
date_obj = self.pool.get('date.tools')
329
db_date_format = date_obj.get_db_date_format(cr, uid, context=context)
330
date_format = date_obj.get_date_format(cr, uid, context=context)
332
for obj in self.browse(cr, uid, ids, context=context):
335
result[obj.id].update({f:False})
336
# composition version
337
if obj.composition_type == 'theoretical':
338
result[obj.id].update({'composition_version': obj.composition_version_txt})
339
elif obj.composition_type == 'real':
340
result[obj.id].update({'composition_version': obj.composition_version_id and obj.composition_version_id.composition_version_txt or ''})
341
# composition_combined_ref_lot: mix between both fields reference and batch number which are exclusive fields
342
if obj.composition_expiry_check:
343
result[obj.id].update({'composition_combined_ref_lot': obj.composition_lot_id.name,
344
'composition_exp': obj.composition_lot_id.life_date})
346
result[obj.id].update({'composition_combined_ref_lot': obj.composition_reference,
347
'composition_exp': obj.composition_ref_exp})
348
# name - ex: ITC - 01/01/2012
349
date = datetime.strptime(obj.composition_creation_date, db_date_format)
350
result[obj.id].update({'name': result[obj.id]['composition_version'] + ' - ' + date.strftime(date_format)})
351
# mandatory nomenclature levels
352
result[obj.id].update({'nomen_manda_0': obj.composition_product_id.nomen_manda_0.id})
353
result[obj.id].update({'nomen_manda_1': obj.composition_product_id.nomen_manda_1.id})
354
result[obj.id].update({'nomen_manda_2': obj.composition_product_id.nomen_manda_2.id})
355
result[obj.id].update({'nomen_manda_3': obj.composition_product_id.nomen_manda_3.id})
356
result[obj.id].update({'nomen_sub_0': obj.composition_product_id.nomen_sub_0.id})
357
result[obj.id].update({'nomen_sub_1': obj.composition_product_id.nomen_sub_1.id})
358
result[obj.id].update({'nomen_sub_2': obj.composition_product_id.nomen_sub_2.id})
359
result[obj.id].update({'nomen_sub_3': obj.composition_product_id.nomen_sub_3.id})
360
result[obj.id].update({'nomen_sub_4': obj.composition_product_id.nomen_sub_4.id})
361
result[obj.id].update({'nomen_sub_5': obj.composition_product_id.nomen_sub_5.id})
364
def copy(self, cr, uid, id, default=None, context=None):
366
change version name. add (copy)
368
- theoretical kit can be copied. version -> version (copy)
369
- real kit with batch number cannot be copied.
370
- real kit without batch but with reference can be copied. reference -> reference (copy)
375
default.update(state='draft')
377
data = self.read(cr, uid, id, ['composition_version_txt', 'composition_type', 'composition_reference', 'composition_lot_id'], context=context)
378
if data['composition_type'] == 'theoretical':
379
version = data['composition_version_txt']
380
default.update(composition_version_txt='%s (copy)'%version, composition_creation_date=time.strftime('%Y-%m-%d'))
381
elif data['composition_type'] == 'real' and data['composition_reference'] and not data['composition_lot_id']:
382
reference = data['composition_reference']
383
default.update(composition_reference='%s (copy)'%reference, composition_creation_date=time.strftime('%Y-%m-%d'))
385
raise osv.except_osv(_('Warning !'), _('Kit Composition List with Batch Number cannot be copied!'))
387
return super(composition_kit, self).copy(cr, uid, id, default, context=context)
389
def unlink(self, cr, uid, ids, context=None):
391
cannot delete composition kit not draft
396
if isinstance(ids, (int, long)):
399
for obj in self.browse(cr, uid, ids, context=context):
400
if obj.state != 'draft':
401
raise osv.except_osv(_('Warning !'), _("Cannot delete Kits not in 'draft' state."))
402
return super(composition_kit, self).unlink(cr, uid, ids, context=context)
404
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
410
# the search view depends on the type we want to display
411
if view_type == 'search':
412
if not context.get('composition_type', False) and not context.get('wizard_composition_type', False):
413
# view search not from a menu -> picking process wizard, we are looking for composition list - by default the theoretical search view is displayed
414
view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'kit', 'view_composition_kit_real_filter')
417
# second level flag for wizards
418
elif context.get('wizard_composition_type', False) == 'theoretical':
419
view = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'kit', 'view_composition_kit_theoretical_filter')
424
result = super(composition_kit, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
425
# columns depending on type - fields from one2many field
426
if view_type == 'form' and context.get('composition_type', False) == 'theoretical':
427
# fields to be modified
428
list = ['<field name="item_lot"', '<field name="item_exp"']
429
replace_text = result['fields']['composition_item_ids']['views']['tree']['arch']
430
replace_text = reduce(lambda x, y: x.replace(y, y + ' invisible="True" '), [replace_text] + list)
431
result['fields']['composition_item_ids']['views']['tree']['arch'] = replace_text
433
list = ['<field name="composition_exp"', '<field name="composition_combined_ref_lot"']
434
# columns from kit composition tree - if we display from theoretical menu or diplay the search view of version_id from real_filter
435
if view_type == 'tree':
436
if context.get('wizard_composition_type', False) == 'theoretical' or (context.get('composition_type', False) == 'theoretical' and not context.get('wizard_composition_type', False)):
437
replace_text = result['arch']
438
replace_text = reduce(lambda x, y: x.replace(y, y + ' invisible="True" '), [replace_text] + list)
439
result['arch'] = replace_text
443
def name_get(self, cr, uid, ids, context=None):
445
override displayed name
450
if isinstance(ids, (int, long)):
454
date_obj = self.pool.get('date.tools')
455
db_date_format = date_obj.get_db_date_format(cr, uid, context=context)
456
date_format = date_obj.get_date_format(cr, uid, context=context)
460
for obj in self.browse(cr, uid, ids, context=context):
461
if obj.composition_type == 'theoretical':
462
date = datetime.strptime(obj.composition_creation_date, db_date_format)
463
name = obj.composition_version + ' - ' + date.strftime(date_format)
465
name = obj.composition_combined_ref_lot
467
res += [(obj.id, name)]
470
def on_change_product_id(self, cr, uid, ids, product_id, context=None):
472
when the product is changed, lot checks are updated - mandatory workaround for attrs use
475
prod_obj = self.pool.get('product.product')
476
res = {'value': {'composition_batch_check': False,
477
'composition_expiry_check': False,
478
'composition_lot_id': False,
479
'composition_exp': False,
480
'composition_reference': False,
481
'composition_ref_exp': False,
482
'composition_version_id': False,
483
'composition_version_txt': False}}
487
data = prod_obj.read(cr, uid, [product_id], ['perishable', 'batch_management'], context=context)[0]
488
res['value']['composition_batch_check'] = data['batch_management']
489
res['value']['composition_expiry_check'] = data['perishable']
492
def on_change_lot_id(self, cr, uid, ids, lot_id, context=None):
494
when the lot is changed, expiry date is updated, so the field is modified before the save happens
497
lot_obj = self.pool.get('stock.production.lot')
498
res = {'value': {'composition_exp': False,
499
'composition_ref_exp': False}}
503
data = lot_obj.read(cr, uid, [lot_id], ['life_date'], context=context)[0]
504
res['value']['composition_exp'] = data['life_date']
507
def _get_composition_kit_from_product_ids(self, cr, uid, ids, context=None):
509
ids represents the ids of product.product objects for which values have changed
511
return the list of ids of composition.kit objects which need to get their fields updated
513
self is product.product object
518
if isinstance(ids, (int, long)):
521
kit_obj = self.pool.get('composition.kit')
522
result = kit_obj.search(cr, uid, [('composition_product_id', 'in', ids)], context=context)
525
def _get_composition_kit_from_lot_ids(self, cr, uid, ids, context=None):
527
ids represents the ids of stock.production.lot objects for which values have changed
529
return the list of ids of composition.kit objects which need to get their fields updated
531
self is stock.production.lot object
536
if isinstance(ids, (int, long)):
539
kit_obj = self.pool.get('composition.kit')
540
result = kit_obj.search(cr, uid, [('composition_lot_id', 'in', ids)], context=context)
543
def onChangeSearchNomenclature(self, cr, uid, ids, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=True, context=None):
544
prod_obj = self.pool.get('product.product')
545
return prod_obj.onChangeSearchNomenclature(cr, uid, ids, position, type, nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, num=num, context=context)
547
def _get_nomen_s(self, cr, uid, ids, fields, *a, **b):
548
prod_obj = self.pool.get('product.template')
549
return prod_obj._get_nomen_s(cr, uid, ids, fields, *a, **b)
551
def _search_nomen_s(self, cr, uid, obj, name, args, context=None):
552
prod_obj = self.pool.get('product.template')
553
return prod_obj._search_nomen_s(cr, uid, obj, name, args, context=context)
555
def _set_expiry_date(self, cr, uid, ids, field, value, arg, context=None):
557
if the kit is linked to a batch management product, we update the expiry date for the correponding batch number
558
else we udpate the composition_ref_exp field
560
if not value or field != 'composition_exp':
564
if isinstance(ids, (int, long)):
567
lot_obj = self.pool.get('stock.production.lot')
569
date_obj = self.pool.get('date.tools')
570
db_date_format = date_obj.get_db_date_format(cr, uid, context=context)
571
date_format = date_obj.get_date_format(cr, uid, context=context)
573
for obj in self.browse(cr, uid, ids, context=context):
574
if obj.composition_expiry_check:
575
# a lot is linked, we update its expiry date
576
lot_obj.write(cr, uid, [obj.composition_lot_id.id], {'life_date': value}, context=context)
577
lot_name = obj.composition_lot_id.name
578
prod_name = obj.composition_product_id.name
579
exp_obj = datetime.strptime(value, db_date_format)
580
lot_obj.log(cr, uid, obj.composition_lot_id.id, _('Expiry Date of Batch Number %s for product %s has been updated to %s.'%(lot_name,prod_name,exp_obj.strftime(date_format))))
582
# not lot because the product is not batch managment, we have a reference instead, we write in composition_ref_exp
583
self.write(cr, uid, ids, {'composition_ref_exp': value}, context=context)
586
_columns = {'composition_type': fields.selection(KIT_COMPOSITION_TYPE, string='Composition Type', readonly=True, required=True),
587
'composition_description': fields.text(string='Composition Description'),
588
'composition_product_id': fields.many2one('product.product', string='Product', required=True, domain=[('type', '=', 'product'), ('subtype', '=', 'kit')]),
589
'composition_version_txt': fields.char(string='Version', size=1024),
590
'composition_version_id': fields.many2one('composition.kit', string='Version'),
591
'composition_creation_date': fields.date(string='Creation Date', required=True),
592
'composition_reference': fields.char(string='Reference', size=1024),
593
'composition_lot_id': fields.many2one('stock.production.lot', string='Batch Nb'),
594
'composition_ref_exp': fields.date(string='Expiry Date for Kit with reference', readonly=True),
595
# 'composition_kit_creation_id': fields.many2one('kit.creation', string='Kitting Order', readonly=True),
596
'composition_item_ids': fields.one2many('composition.item', 'item_kit_id', string='Items'),
597
'active': fields.boolean('Active', readonly=True),
598
'state': fields.selection(KIT_STATE, string='State', readonly=True, required=True),
600
'composition_batch_check': fields.related('composition_product_id', 'batch_management', type='boolean', string='Batch Number Mandatory', readonly=True, store=False),
601
# expiry is always true if batch_check is true. we therefore use expry_check for now in the code
602
'composition_expiry_check': fields.related('composition_product_id', 'perishable', type='boolean', string='Expiry Date Mandatory', readonly=True, store=False),
604
'name': fields.function(_vals_get, method=True, type='char', size=1024, string='Name', multi='get_vals',
605
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),}),
606
'composition_version': fields.function(_vals_get, method=True, type='char', size=1024, string='Version', multi='get_vals',
607
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_version_txt', 'composition_version_id'], 10),}),
608
'composition_exp': fields.function(_vals_get, fnct_inv=_set_expiry_date, method=True, type='date', size=1024, string='Expiry Date', multi='get_vals', readonly=True,
609
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_ref_exp', 'composition_lot_id'], 10),
610
'stock.production.lot': (_get_composition_kit_from_lot_ids, ['life_date'], 10)}),
611
'composition_combined_ref_lot': fields.function(_vals_get, method=True, type='char', size=1024, string='Ref/Batch Nb', multi='get_vals',
612
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_lot_id', 'composition_reference'], 10),}),
614
'nomen_manda_0': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Main Type', multi='get_vals', readonly=True, select=True,
615
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
616
'product.template': (_get_composition_kit_from_product_ids, ['nomen_manda_0', 'nomen_manda_1', 'nomen_manda_2', 'nomen_manda_3'], 10),}),
617
'nomen_manda_1': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Group', multi='get_vals', readonly=True, select=True,
618
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
619
'product.template': (_get_composition_kit_from_product_ids, ['nomen_manda_0', 'nomen_manda_1', 'nomen_manda_2', 'nomen_manda_3'], 10),}),
620
'nomen_manda_2': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Family', multi='get_vals', readonly=True, select=True,
621
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
622
'product.template': (_get_composition_kit_from_product_ids, ['nomen_manda_0', 'nomen_manda_1', 'nomen_manda_2', 'nomen_manda_3'], 10),}),
623
'nomen_manda_3': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Root', multi='get_vals', readonly=True, select=True,
624
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
625
'product.template': (_get_composition_kit_from_product_ids, ['nomen_manda_0', 'nomen_manda_1', 'nomen_manda_2', 'nomen_manda_3'], 10),}),
626
'nomen_manda_0_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Main Type', fnct_search=_search_nomen_s, multi="nom_s"),
627
'nomen_manda_1_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Group', fnct_search=_search_nomen_s, multi="nom_s"),
628
'nomen_manda_2_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Family', fnct_search=_search_nomen_s, multi="nom_s"),
629
'nomen_manda_3_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Root', fnct_search=_search_nomen_s, multi="nom_s"),
630
'nomen_sub_0': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 1', multi='get_vals', readonly=True, select=True,
631
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
632
'product.template': (_get_composition_kit_from_product_ids, ['nomen_sub_0', 'nomen_sub_1', 'nomen_sub_2', 'nomen_sub_3', 'nomen_sub_4', 'nomen_sub_5'], 10),}),
633
'nomen_sub_1': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 2', multi='get_vals', readonly=True, select=True,
634
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
635
'product.template': (_get_composition_kit_from_product_ids, ['nomen_sub_0', 'nomen_sub_1', 'nomen_sub_2', 'nomen_sub_3', 'nomen_sub_4', 'nomen_sub_5'], 10),}),
636
'nomen_sub_2': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 3', multi='get_vals', readonly=True, select=True,
637
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
638
'product.template': (_get_composition_kit_from_product_ids, ['nomen_sub_0', 'nomen_sub_1', 'nomen_sub_2', 'nomen_sub_3', 'nomen_sub_4', 'nomen_sub_5'], 10),}),
639
'nomen_sub_3': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 4', multi='get_vals', readonly=True, select=True,
640
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
641
'product.template': (_get_composition_kit_from_product_ids, ['nomen_sub_0', 'nomen_sub_1', 'nomen_sub_2', 'nomen_sub_3', 'nomen_sub_4', 'nomen_sub_5'], 10),}),
642
'nomen_sub_4': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 5', multi='get_vals', readonly=True, select=True,
643
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
644
'product.template': (_get_composition_kit_from_product_ids, ['nomen_sub_0', 'nomen_sub_1', 'nomen_sub_2', 'nomen_sub_3', 'nomen_sub_4', 'nomen_sub_5'], 10),}),
645
'nomen_sub_5': fields.function(_vals_get, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 6', multi='get_vals', readonly=True, select=True,
646
store= {'composition.kit': (lambda self, cr, uid, ids, c=None: ids, ['composition_product_id'], 10),
647
'product.template': (_get_composition_kit_from_product_ids, ['nomen_sub_0', 'nomen_sub_1', 'nomen_sub_2', 'nomen_sub_3', 'nomen_sub_4', 'nomen_sub_5'], 10),}),
648
'nomen_sub_0_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 1', fnct_search=_search_nomen_s, multi="nom_s"),
649
'nomen_sub_1_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 2', fnct_search=_search_nomen_s, multi="nom_s"),
650
'nomen_sub_2_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 3', fnct_search=_search_nomen_s, multi="nom_s"),
651
'nomen_sub_3_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 4', fnct_search=_search_nomen_s, multi="nom_s"),
652
'nomen_sub_4_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 5', fnct_search=_search_nomen_s, multi="nom_s"),
653
'nomen_sub_5_s': fields.function(_get_nomen_s, method=True, type='many2one', relation='product.nomenclature', string='Sub Class 6', fnct_search=_search_nomen_s, multi="nom_s"),
656
_defaults = {'composition_creation_date': lambda *a: time.strftime('%Y-%m-%d'),
657
'composition_type': lambda s, cr, uid, c: c.get('composition_type', False),
658
'composition_product_id': lambda s, cr, uid, c: c.get('composition_product_id', False),
659
'composition_lot_id': lambda s, cr, uid, c: c.get('composition_lot_id', False),
660
'composition_exp': lambda s, cr, uid, c: c.get('composition_exp', False),
661
'composition_batch_check': lambda s, cr, uid, c: c.get('composition_batch_check', False),
662
'composition_expiry_check': lambda s, cr, uid, c: c.get('composition_expiry_check', False),
667
_order = 'composition_creation_date desc'
669
def _composition_kit_constraint(self, cr, uid, ids, context=None):
671
constraint on kit composition - two kits
676
if isinstance(ids, (int, long)):
679
for obj in self.browse(cr, uid, ids, context=context):
681
if obj.composition_product_id.type != 'product' or obj.composition_product_id.subtype != 'kit':
682
raise osv.except_osv(_('Warning !'), _('Only Kit products can be used for kits.'))
683
# theoretical constraints
684
if obj.composition_type == 'theoretical':
685
search_ids = self.search(cr, uid, [('id', '!=', obj.id),
686
('composition_product_id', '=', obj.composition_product_id.id),
687
('composition_version_txt', '=ilike', obj.composition_version_txt),
688
('composition_creation_date', '=', obj.composition_creation_date)], context=context)
690
#print self.read(cr, uid, ids, ['composition_product_id', 'composition_version_txt', 'composition_creation_date'], context=context)
691
raise osv.except_osv(_('Warning !'), _('The dataset (Product - Version - Creation Date) must be unique.'))
692
# constraint on lot_id/reference/expiry date - forbidden for theoretical
693
if obj.composition_reference or obj.composition_lot_id or obj.composition_exp or obj.composition_ref_exp:
694
raise osv.except_osv(_('Warning !'), _('Composition Reference / Batch Number / Expiry date is not available for Theoretical Kit.'))
695
# constraint on version_id - forbidden for theoretical
696
if obj.composition_version_id:
697
raise osv.except_osv(_('Warning !'), _('Composition Version Object is not available for Theoretical Kit.'))
700
if obj.composition_type == 'real':
701
# constraint on lot_id/reference - mandatory for real kit
702
if obj.composition_batch_check or obj.composition_expiry_check:
703
if obj.composition_reference:
704
raise osv.except_osv(_('Warning !'), _('Composition List with Batch Management Product does not allow Reference.'))
705
if not obj.composition_lot_id:
706
raise osv.except_osv(_('Warning !'), _('Composition List with Batch Management Product needs Batch Number.'))
707
if obj.composition_ref_exp:
708
raise osv.except_osv(_('Warning !'), _('Composition List with Batch Management Product does not allow Reference based Expiry Date.'))
710
if not obj.composition_reference:
711
raise osv.except_osv(_('Warning !'), _('Composition List without Batch Management Product needs Reference.'))
712
if obj.composition_lot_id:
713
raise osv.except_osv(_('Warning !'), _('Composition List without Batch Management Product does not allow Batch Number.'))
714
# real composition must always be active
716
raise osv.except_osv(_('Warning !'), _('Composition List cannot be inactive.'))
717
# check that the selected version corresponds to the selected product
718
if obj.composition_version_id and obj.composition_version_id.composition_product_id.id != obj.composition_product_id.id:
719
raise osv.except_osv(_('Warning !'), _('Selected Version is for a different product.'))
723
_constraints = [(_composition_kit_constraint, 'Constraint error on Composition Kit.', []),
725
_sql_constraints = [('unique_composition_kit_real_ref', "unique(composition_product_id,composition_reference)", 'Kit Composition List Reference must be unique for a given product.'),
726
('unique_composition_kit_real_lot', "unique(composition_lot_id)", 'Batch Number can only be used by one Kit Composition List.'),
732
class composition_item(osv.osv):
734
kit composition items representing kit parts
736
_name = 'composition.item'
738
def create(self, cr, uid, vals, context=None):
740
force writing of expired_date which is readonly for batch management products
743
prod_obj = self.pool.get('product.product')
744
prodlot_obj = self.pool.get('stock.production.lot')
745
if 'item_product_id' in vals:
746
if vals['item_product_id']:
747
product_id = vals['item_product_id']
748
data = prod_obj.read(cr, uid, [product_id], ['perishable', 'batch_management'], context=context)[0]
749
management = data['batch_management']
750
perishable = data['perishable']
751
# if management and we have a lot_id, we fill the expiry date
752
if management and vals.get('item_lot'):
753
prodlot_id = vals.get('item_lot')
754
prod_ids = prodlot_obj.search(cr, uid, [('name', '=', prodlot_id),
755
('type', '=', 'standard'),
756
('product_id', '=', product_id)], context=context)
757
# if it exists, we set the date
759
prodlot_id = prod_ids[0]
760
data = prodlot_obj.read(cr, uid, [prodlot_id], ['life_date'], context=context)
761
expired_date = data[0]['life_date']
762
vals.update({'item_exp': expired_date})
764
# nothing special here
767
# not perishable nor management, exp and lot are False
768
vals.update(item_lot=False, item_exp=False)
770
# product is False, exp and lot are set to False
771
vals.update(item_lot=False, item_exp=False)
772
return super(composition_item, self).create(cr, uid, vals, context=context)
774
def write(self, cr, uid, ids, vals, context=None):
776
force writing of expired_date which is readonly for batch management products
779
prod_obj = self.pool.get('product.product')
780
prodlot_obj = self.pool.get('stock.production.lot')
781
if 'item_product_id' in vals:
782
if vals['item_product_id']:
783
product_id = vals['item_product_id']
784
data = prod_obj.read(cr, uid, [product_id], ['perishable', 'batch_management'], context=context)[0]
785
management = data['batch_management']
786
perishable = data['perishable']
787
# if management and we have a lot_id, we fill the expiry date
788
if management and vals.get('item_lot'):
789
prodlot_id = vals.get('item_lot')
790
prod_ids = prodlot_obj.search(cr, uid, [('name', '=', prodlot_id),
791
('type', '=', 'standard'),
792
('product_id', '=', product_id)], context=context)
793
# if it exists, we set the date
795
prodlot_id = prod_ids[0]
796
data = prodlot_obj.read(cr, uid, [prodlot_id], ['life_date'], context=context)
797
expired_date = data[0]['life_date']
798
vals.update({'item_exp': expired_date})
800
# nothing special here
803
# not perishable nor management, exp and lot are False
804
vals.update(item_lot=False, item_exp=False)
806
# product is False, exp and lot are set to False
807
vals.update(item_lot=False, item_exp=False)
808
return super(composition_item, self).write(cr, uid, ids, vals, context=context)
810
def on_product_change(self, cr, uid, ids, product_id, context=None):
812
product is changed, we update the UoM
815
prod_obj = self.pool.get('product.product')
816
result = {'value': {'item_uom_id': False,
818
'hidden_perishable_mandatory': False,
819
'hidden_batch_management_mandatory': False,
824
product = prod_obj.browse(cr, uid, product_id, context=context)
825
result['value']['item_uom_id'] = product.uom_po_id.id
826
result['value']['hidden_perishable_mandatory'] = product.perishable
827
result['value']['hidden_batch_management_mandatory'] = product.batch_management
831
def on_lot_change(self, cr, uid, ids, product_id, prodlot_id, context=None):
833
if lot exists in the system the date is filled in
835
prodlot_id is the NAME of the production lot
838
prod_obj = self.pool.get('product.product')
839
prodlot_obj = self.pool.get('stock.production.lot')
841
result = {'value': {}}
843
data = prod_obj.read(cr, uid, [product_id], ['perishable', 'batch_management'], context=context)[0]
844
management = data['batch_management']
845
perishable = data['perishable']
846
if management and prodlot_id:
847
prod_ids = prodlot_obj.search(cr, uid, [('name', '=', prodlot_id),
848
('type', '=', 'standard'),
849
('product_id', '=', product_id)], context=context)
850
# if it exists, we set the date
852
prodlot_id = prod_ids[0]
853
result['value'].update(item_exp=prodlot_obj.browse(cr, uid, prodlot_id, context=context).life_date)
857
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
864
result = super(composition_item, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
865
# columns depending on type
866
if view_type == 'tree' and context.get('composition_type', False) == 'theoretical':
867
# fields to be modified
868
list = ['<field name="item_lot"', '<field name="item_exp"']
869
replace_text = result['arch']
870
replace_text = reduce(lambda x, y: x.replace(y, y+ ' invisible="True" '), [replace_text] + list)
871
result['arch'] = replace_text
875
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
877
multi fields function method
882
if isinstance(ids, (int, long)):
886
for obj in self.browse(cr, uid, ids, context=context):
889
result[obj.id].update({'name': obj.item_product_id.name})
891
result[obj.id].update({'item_kit_version': obj.item_kit_id.composition_version})
893
result[obj.id].update({'item_kit_type': obj.item_kit_id.composition_type})
895
result[obj.id].update({'state': obj.item_kit_id.state})
897
result[obj.id].update({'hidden_batch_management_mandatory': obj.item_product_id.batch_management})
899
result[obj.id].update({'hidden_perishable_mandatory': obj.item_product_id.perishable})
902
def name_get(self, cr, uid, ids, context=None):
904
override displayed name
909
if isinstance(ids, (int, long)):
913
date_obj = self.pool.get('date.tools')
914
date_format = date_obj.get_date_format(cr, uid, context=context)
918
for obj in self.browse(cr, uid, ids, context=context):
919
name = obj.item_product_id.name
920
res += [(obj.id, name)]
923
def _get_composition_item_ids(self, cr, uid, ids, context=None):
925
ids represents the ids of composition.kit objects for which values have changed
927
return the list of ids of composition.item objects which need to get their fields updated
929
self is an composition.kit object
934
if isinstance(ids, (int, long)):
937
item_obj = self.pool.get('composition.item')
938
result = item_obj.search(cr, uid, [('item_kit_id', 'in', ids)], context=context)
941
_columns = {'item_module': fields.char(string='Module', size=1024),
942
'item_product_id': fields.many2one('product.product', string='Product', required=True),
943
'item_qty': fields.float(string='Qty', digits_compute=dp.get_precision('Product UoM'), required=True),
944
'item_uom_id': fields.many2one('product.uom', string='UoM', required=True),
945
'item_lot': fields.char(string='Batch Nb', size=1024),
946
'item_exp': fields.date(string='Expiry Date'),
947
'item_kit_id': fields.many2one('composition.kit', string='Kit', ondelete='cascade', required=True, readonly=True),
948
'item_description': fields.text(string='Item Description'),
949
'item_stock_move_id': fields.many2one('stock.move', string='Kitting Order Stock Move', readonly=True, help='This field represents the stock move corresponding to this item for Kit production.'),
951
'name': fields.function(_vals_get, method=True, type='char', size=1024, string='Name', multi='get_vals',
952
store= {'composition.item': (lambda self, cr, uid, ids, c=None: ids, ['item_product_id'], 10),}),
953
'item_kit_version': fields.function(_vals_get, method=True, type='char', size=1024, string='Kit Version', multi='get_vals',
954
store= {'composition.item': (lambda self, cr, uid, ids, c=None: ids, ['item_kit_id'], 10),
955
'composition.kit': (_get_composition_item_ids, ['composition_version_txt', 'composition_version_id'], 10)}),
956
'item_kit_type': fields.function(_vals_get, method=True, type='char', size=1024, string='Kit Type', multi='get_vals',
957
store= {'composition.item': (lambda self, cr, uid, ids, c=None: ids, ['item_kit_id'], 10),
958
'composition.kit': (_get_composition_item_ids, ['composition_type'], 10)}),
959
'state': fields.function(_vals_get, method=True, type='selection', selection=KIT_STATE, string='State', readonly=True, multi='get_vals',
960
store= {'composition.item': (lambda self, cr, uid, ids, c=None: ids, ['item_kit_id'], 10),
961
'composition.kit': (_get_composition_item_ids, ['state'], 10)}),
962
'hidden_perishable_mandatory': fields.function(_vals_get, method=True, type='boolean', string='Exp', multi='get_vals', store=False, readonly=True),
963
'hidden_batch_management_mandatory': fields.function(_vals_get, method=True, type='boolean', string='B.Num', multi='get_vals', store=False, readonly=True),
966
_defaults = {'hidden_batch_management_mandatory': False,
967
'hidden_perishable_mandatory': False}
969
def _composition_item_constraint(self, cr, uid, ids, context=None):
971
constraint on item composition
976
if isinstance(ids, (int, long)):
979
for obj in self.browse(cr, uid, ids, context=context):
980
if not obj.hidden_perishable_mandatory:
981
# no lot or date management product
983
# not perishable nor batch management - no item_lot nor item_exp
984
raise osv.except_osv(_('Warning !'), _('Only Batch Number Mandatory Product can specify Batch Number.'))
986
# not perishable nor batch management - no item_lot nor item_exp
987
raise osv.except_osv(_('Warning !'), _('Only Batch Number Mandatory or Expiry Date Mandatory can specify Expiry Date.'))
991
_constraints = [(_composition_item_constraint, 'Constraint error on Composition Item.', []),]
997
class product_product(osv.osv):
999
add a constraint - a product of subtype 'kit' cannot be perishable only, should be batch management or nothing
1001
_inherit = 'product.product'
1003
def _kit_product_constraints(self, cr, uid, ids, context=None):
1005
constraint on product
1007
# Some verifications
1010
if isinstance(ids, (int, long)):
1013
for obj in self.browse(cr, uid, ids, context=context):
1015
if obj.type == 'product' and obj.subtype == 'kit':
1016
if obj.perishable and not obj.batch_management:
1017
raise osv.except_osv(_('Warning !'), _('The Kit product cannot be Expiry Date Mandatory only.'))
1021
_constraints = [(_kit_product_constraints, 'Constraint error on Kit Product.', []),
1027
class product_nomenclature(osv.osv):
1031
def _getNumberOfProducts(self, cr, uid, ids, field_name, arg, context=None):
1033
_inherit = 'product.nomenclature'
1035
def _getNumberOfProducts(self, cr, uid, ids, field_name, arg, context=None):
1037
check if we are concerned with composition kit, if we do, we return the number of concerned kit, not product
1039
# Some verifications
1042
if isinstance(ids, (int, long)):
1045
if context.get('composition_type', False):
1046
composition_type = context.get('composition_type')
1048
for nomen in self.browse(cr, uid, ids, context=context):
1050
if nomen.type == 'mandatory':
1051
name = 'nomen_manda_%s'%nomen.level
1052
if nomen.type == 'optional':
1053
name = 'nomen_sub_%s'%nomen.sub_level
1054
kit_ids = self.pool.get('composition.kit').search(cr, uid, [('composition_type', '=', composition_type), (name, '=', nomen.id)], context=context)
1058
res[nomen.id] = len(kit_ids)
1061
return super(product_nomenclature, self)._getNumberOfProducts(cr, uid, ids, field_name, arg, context=context)
1063
_columns = {'number_of_products': fields.function(_getNumberOfProducts, type='integer', method=True, store=False, string='Number of Products', readonly=True),
1066
product_nomenclature()
1069
class stock_move(osv.osv):
1071
add the new method self.create_composition_list
1073
_inherit= 'stock.move'
1075
def create_composition_list(self, cr, uid, ids, context=None):
1077
return the form view of composition_list (real) with corresponding values from the context
1079
# Some verifications
1082
if isinstance(ids, (int, long)):
1085
obj = self.browse(cr, uid, ids[0], context=context)
1086
composition_type = 'real'
1087
composition_product_id = obj.product_id.id
1088
composition_lot_id = obj.prodlot_id and obj.prodlot_id.id or False
1089
composition_exp = obj.expired_date
1090
composition_batch_check = obj.product_id.batch_management
1091
composition_expiry_check = obj.product_id.perishable
1093
return {'name': 'Kit Composition List',
1095
'view_type': 'form',
1096
'view_mode': 'form,tree',
1097
'res_model': 'composition.kit',
1099
'type': 'ir.actions.act_window',
1102
'domain': "[('composition_type', '=', 'real')]",
1103
'context': dict(context,
1104
composition_type=composition_type,
1105
composition_product_id=composition_product_id,
1106
composition_lot_id=composition_lot_id,
1107
composition_exp=composition_exp, # set so we do not need to wait the save to see the expiry date
1108
composition_batch_check=composition_batch_check,
1109
composition_expiry_check=composition_expiry_check,
1113
def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
1115
hook to update defaults data
1117
# variable parameters
1118
move = kwargs.get('move')
1119
assert move, 'missing move'
1120
partial_datas = kwargs.get('partial_datas')
1121
assert partial_datas, 'missing partial_datas'
1123
# calling super method
1124
defaults = super(stock_move, self)._do_partial_hook(cr, uid, ids, context, *args, **kwargs)
1125
assert defaults is not None
1127
kit_id = partial_datas.get('move%s'%(move.id), False).get('composition_list_id')
1129
defaults.update({'composition_list_id': kit_id})
1133
_columns = {'composition_list_id': fields.many2one('composition.kit', string='Kit', readonly=True)}
1138
class stock_location(osv.osv):
1140
add a new reservation method, taking production lot into account
1142
_inherit = 'stock.location'
1144
def compute_availability(self, cr, uid, ids, consider_child_locations, product_id, uom_id, context=None):
1146
call stock computation function
1148
# Some verifications
1151
if isinstance(ids, (int, long)):
1154
loc_obj = self.pool.get('stock.location')
1155
# do we want the child location
1156
stock_context = dict(context, compute_child=consider_child_locations)
1157
# we check for the available qty (in:done, out: assigned, done)
1158
res = loc_obj._product_reserve_lot(cr, uid, ids, product_id, uom_id, context=stock_context, lock=True)
1162
def _product_reserve_lot(self, cr, uid, ids, product_id, uom_id, context=None, lock=False):
1164
refactoring of original reserver method, taking production lot into account
1166
returning the original list-tuple structure + the total qty in each location
1173
pool_uom = self.pool.get('product.uom')
1174
# location ids depends on the compute_child parameter from the context
1175
if context.get('compute_child', True):
1176
location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)], context=context)
1182
data = {'fefo': fefo_list, 'total': 0.0}
1184
for id in location_ids:
1185
# set up default value
1186
data.setdefault(id, {}).setdefault('total', 0.0)
1187
# lock the database if needed
1190
# Must lock with a separate select query because FOR UPDATE can't be used with
1191
# aggregation/group by's (when individual rows aren't identifiable).
1192
# We use a SAVEPOINT to be able to rollback this part of the transaction without
1193
# failing the whole transaction in case the LOCK cannot be acquired.
1194
cr.execute("SAVEPOINT stock_location_product_reserve")
1195
cr.execute("""SELECT id FROM stock_move
1196
WHERE product_id=%s AND
1198
(location_dest_id=%s AND
1203
location_dest_id<>%s AND
1204
state in ('done', 'assigned'))
1206
FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id), log_exceptions=False)
1208
# Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
1209
# so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
1210
# state, we return False as if the products were not available, and log it:
1211
cr.execute("ROLLBACK TO stock_location_product_reserve_lot")
1212
logger = logging.getLogger('stock.location')
1213
logger.warn("Failed attempt to reserve product %s, likely due to another transaction already in progress. Next attempt is likely to work. Detailed error available at DEBUG level.", product_id)
1214
logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
1217
# SQL request is FEFO by default
1218
# TODO merge different UOM directly in SQL statement
1219
# example in class stock_report_prodlots_virtual(osv.osv): in report_stock_virtual.py
1220
# class report_stock_inventory(osv.osv): in specific_rules.py
1222
SELECT subs.product_uom, subs.prodlot_id, subs.expired_date, sum(subs.product_qty) AS product_qty FROM
1223
(SELECT product_uom, prodlot_id, expired_date, sum(product_qty) AS product_qty
1225
WHERE location_dest_id=%s AND
1229
GROUP BY product_uom, prodlot_id, expired_date
1233
SELECT product_uom, prodlot_id, expired_date, -sum(product_qty) AS product_qty
1235
WHERE location_id=%s AND
1236
location_dest_id<>%s AND
1238
state in ('done', 'assigned')
1239
GROUP BY product_uom, prodlot_id, expired_date) as subs
1240
GROUP BY product_uom, prodlot_id, expired_date
1241
ORDER BY prodlot_id asc, expired_date asc
1243
(id, id, product_id, id, id, product_id))
1244
results = cr.dictfetchall()
1245
# merge results according to uom if needed
1247
# consolidates the uom
1248
amount = pool_uom._compute_qty(cr, uid, r['product_uom'], r['product_qty'], uom_id)
1249
# total for all locations
1250
total = data.setdefault('total', 0.0)
1252
data.update({'total': total})
1253
# fill the data structure, total value for location
1254
loc_tot = data.setdefault(id, {}).setdefault('total', 0.0)
1256
data.setdefault(id, {}).update({'total': loc_tot})
1258
lot_tot = data.setdefault(id, {}).setdefault(r['prodlot_id'], {}).setdefault('total', 0.0)
1260
data.setdefault(id, {}).setdefault(r['prodlot_id'], {}).update({'total': lot_tot, 'date': r['expired_date']})
1261
# update the fefo list - will be sorted when all location has been treated - we can test only the last one, thanks to ORDER BY sql request
1262
# only positive amount are taken into account
1264
# FEFO logic is only meaningful if a production lot is associated
1265
if fefo_list and fefo_list[-1]['location_id'] == id and fefo_list[-1]['prodlot_id'] == r['prodlot_id']:
1266
# simply update the qty
1268
fefo_list[-1].update({'qty': lot_tot})
1273
fefo_list.append({'location_id': id,
1275
'expired_date': r['expired_date'],
1276
'prodlot_id': r['prodlot_id'],
1277
'product_id': product_id,
1279
# global FEFO sorting
1280
data['fefo'] = sorted(fefo_list, cmp=lambda x, y: cmp(x.get('expired_date'), y.get('expired_date')), reverse=False)
1286
class stock_picking(osv.osv):
1288
treat the composition list
1290
_inherit = 'stock.picking'
1292
def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
1294
hook to update defaults data
1296
# variable parameters
1297
move = kwargs.get('move')
1298
assert move, 'missing move'
1299
partial_datas = kwargs.get('partial_datas')
1300
assert partial_datas, 'missing partial_datas'
1302
# calling super method
1303
defaults = super(stock_picking, self)._do_partial_hook(cr, uid, ids, context, *args, **kwargs)
1304
kit_id = partial_datas.get('move%s'%(move.id), False).get('composition_list_id')
1306
defaults.update({'composition_list_id': kit_id})
1313
class purchase_order_line(osv.osv):
1315
add theoretical de-kitting capabilities
1317
_inherit = 'purchase.order.line'
1319
def de_kitting(self, cr, uid, ids, context=None):
1321
open theoretical kit selection
1326
name = _("Replacement Items Selection")
1327
model = 'kit.selection'
1329
wiz_obj = self.pool.get('wizard')
1330
# this purchase order line replacement function can only be used when the po is in state ('confirmed', 'Validated'),
1331
for obj in self.browse(cr, uid, ids, context=context):
1332
if obj.po_state_stored != 'confirmed':
1333
raise osv.except_osv(_('Warning !'), _('Purchase order line kit replacement with components function is only available for Validated state.'))
1334
# open the selected wizard
1335
data = self.read(cr, uid, ids, ['product_id'], context=context)[0]
1336
product_id = data['product_id'][0]
1337
res = wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=dict(context,
1338
product_id=product_id))
1341
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
1343
multi fields function method
1345
# Some verifications
1348
if isinstance(ids, (int, long)):
1351
kit_obj = self.pool.get('composition.kit')
1353
for obj in self.browse(cr, uid, ids, context=context):
1354
result[obj.id] = {'kit_pol_check': False}
1355
# we want the possibility to explose the kit within the purchase order
1356
# - the product is a kit AND
1357
# - at least one theoretical kit exists for this product - is displayed anyway, because the user can now add products not from the theoretical template
1358
product = obj.product_id
1359
if product.type == 'product' and product.subtype == 'kit':
1360
result[obj.id].update({'kit_pol_check': True})
1361
# kit_ids = kit_obj.search(cr, uid, [('composition_type', '=', 'theoretical'), ('state', '=', 'completed'), ('composition_product_id', '=', product.id)], context=context)
1363
# result[obj.id].update({'kit_pol_check': True})
1366
_columns = {'kit_pol_check' : fields.function(_vals_get, method=True, string='Kit Mem Check', type='boolean', readonly=True, multi='get_vals_kit'),
1369
purchase_order_line()