~unifield-team/unifield-wm/us-826

« back to all changes in this revision

Viewing changes to kit/kit.py

  • Committer: jf
  • Date: 2012-06-13 12:43:21 UTC
  • mfrom: (827.5.11 uf-635)
  • Revision ID: jf@tempo4-20120613124321-2b8cwgl86gyy2tb7
UF-635 [DEV] Documents workflow: Graphic representation
lp:~unifield-team/unifield-wm/uf-635 revno 838

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
##############################################################################
 
3
#
 
4
#    OpenERP, Open Source Management Solution
 
5
#    Copyright (C) 2011 MSF, TeMPO Consulting
 
6
#
 
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.
 
11
#
 
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.
 
16
#
 
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/>.
 
19
#
 
20
##############################################################################
 
21
 
 
22
from osv import osv, fields
 
23
from tools.translate import _
 
24
import netsvc
 
25
from datetime import datetime, timedelta
 
26
from dateutil.relativedelta import relativedelta
 
27
import decimal_precision as dp
 
28
import logging
 
29
import tools
 
30
import time
 
31
from os import path
 
32
 
 
33
KIT_COMPOSITION_TYPE = [('theoretical', 'Theoretical'),
 
34
                        ('real', 'Real'),
 
35
                        ]
 
36
 
 
37
KIT_STATE = [('draft', 'Draft'),
 
38
             ('in_production', 'In Production'),
 
39
             ('completed', 'Completed'),
 
40
             ('done', 'Closed'),
 
41
             ]
 
42
 
 
43
class composition_kit(osv.osv):
 
44
    '''
 
45
    kit composition class, representing both theoretical composition and actual ones
 
46
    '''
 
47
    _name = 'composition.kit'
 
48
    
 
49
    def get_default_expiry_date(self, cr, uid, ids, context=None):
 
50
        '''
 
51
        default value for kits
 
52
        '''
 
53
        return '9999-01-01'
 
54
    
 
55
    def _compute_expiry_date(self, cr, uid, ids, context=None):
 
56
        '''
 
57
        compute the expiry date of real composition.kit based on items
 
58
        '''
 
59
        # date tools object
 
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)
 
63
        
 
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'
 
66
            expiry_date = False
 
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:
 
71
                if item.item_exp:
 
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
 
74
            if not expiry_date:
 
75
                expiry_date = self.get_default_expiry_date(cr, uid, ids, context=context)
 
76
        
 
77
        return expiry_date
 
78
    
 
79
    def modify_expiry_date(self, cr, uid, ids, context=None):
 
80
        '''
 
81
        open modify expiry date wizard
 
82
        '''
 
83
        # basic check
 
84
        if context is None:
 
85
            context = {}
 
86
        # data
 
87
        name = _("Modify Expiry Date")
 
88
        model = 'modify.expiry.date'
 
89
        step = 'default'
 
90
        wiz_obj = self.pool.get('wizard')
 
91
        # get the date
 
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,
 
96
                                                                                                kit_id=ids[0],
 
97
                                                                                                date=date))
 
98
        return res
 
99
    
 
100
    def mark_as_completed(self, cr, uid, ids, context=None):
 
101
        '''
 
102
        button function
 
103
        set the state to 'completed'
 
104
        '''
 
105
        # Some verifications
 
106
        if context is None:
 
107
            context = {}
 
108
        if isinstance(ids, (int, long)):
 
109
            ids = [ids]
 
110
            
 
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.'))
 
114
            if not obj.active:
 
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)
 
120
        return True
 
121
    
 
122
    def mark_as_inactive(self, cr, uid, ids, context=None):
 
123
        '''
 
124
        button function
 
125
        set the active flag to False
 
126
        '''
 
127
        # Some verifications
 
128
        if context is None:
 
129
            context = {}
 
130
        if isinstance(ids, (int, long)):
 
131
            ids = [ids]
 
132
            
 
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)
 
137
        return True
 
138
    
 
139
    def mark_as_active(self, cr, uid, ids, context=None):
 
140
        '''
 
141
        button function
 
142
        set the active flag to False
 
143
        '''
 
144
        # Some verifications
 
145
        if context is None:
 
146
            context = {}
 
147
        if isinstance(ids, (int, long)):
 
148
            ids = [ids]
 
149
            
 
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)
 
154
        return True
 
155
    
 
156
    def close_kit(self, cr, uid, ids, context=None):
 
157
        '''
 
158
        button function
 
159
        set the state to 'done'
 
160
        '''
 
161
        # Some verifications
 
162
        if context is None:
 
163
            context = {}
 
164
        if isinstance(ids, (int, long)):
 
165
            ids = [ids]
 
166
            
 
167
        self.write(cr, uid, ids, {'state': 'done'}, context=context)
 
168
        return True
 
169
    
 
170
    def reset_to_version(self, cr, uid, ids, context=None):
 
171
        '''
 
172
        open confirmation wizard
 
173
        '''
 
174
        # data
 
175
        name = _("Reset Items to Version Reference. Are you sure?")
 
176
        model = 'confirm'
 
177
        step = 'default'
 
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'
 
181
        args = [ids]
 
182
        kwargs = {}
 
183
        # to reset to version
 
184
        for obj in self.browse(cr, uid, ids, context=context):
 
185
            # must be a real kit
 
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.'))
 
191
        
 
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,
 
196
                                                                                                          'func': func,
 
197
                                                                                                          'args': args,
 
198
                                                                                                          'kwargs': kwargs}))
 
199
        return res
 
200
    
 
201
    def do_reset_to_version(self, cr, uid, ids, context=None):
 
202
        '''
 
203
        remove all items and create one item for each item from the referenced version
 
204
        '''
 
205
        # objects
 
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,
 
221
                          }
 
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',
 
226
                    'view_type': 'form',
 
227
                    'res_model': 'composition.kit',
 
228
                    'res_id': obj.id,
 
229
                    'type': 'ir.actions.act_window',
 
230
                    'target': 'dummy',
 
231
                    'domain': [('composition_type', '=', 'real')],
 
232
                    'context': {'composition_type':'real'},
 
233
                    }
 
234
    
 
235
    def _generate_item_mirror_objects(self, cr, uid, ids, wizard_data, context=None):
 
236
        """
 
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).
 
239
        """
 
240
        if context is None:
 
241
            context = {}
 
242
        # objects
 
243
        mirror_obj = self.pool.get('substitute.item.mirror')
 
244
        # returned list, list of created ids
 
245
        result = []
 
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,
 
262
                          }
 
263
                id = mirror_obj.create(cr, uid, values, context=context)
 
264
                result.append(id)
 
265
        return result
 
266
    
 
267
    def substitute_items(self, cr, uid, ids, context=None):
 
268
        '''
 
269
        substitute lines from the composition kit with created new lines
 
270
        '''
 
271
        # we need the context for the wizard switch
 
272
        if context is None:
 
273
            context = {}
 
274
        # data
 
275
        name = _("Substitute Kit Items")
 
276
        model = 'substitute'
 
277
        step = 'substitute'
 
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'])
 
285
        return res
 
286
    
 
287
    def do_substitute(self, cr, uid, ids, context=None):
 
288
        '''
 
289
        call the modify expiry date window for possible modification of expiry date
 
290
        '''
 
291
        res = self.modify_expiry_date(cr, uid, ids, context=context)
 
292
        return res
 
293
        
 
294
    def de_kitting(self, cr, uid, ids, context=None):
 
295
        '''
 
296
        explode the kit, preselecting all mirror items
 
297
        '''
 
298
        # we need the context for the wizard switch
 
299
        if context is None:
 
300
            context = {}
 
301
        # data
 
302
        name = _("De-Kitting")
 
303
        model = 'substitute'
 
304
        step = '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)
 
314
        return res
 
315
    
 
316
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
317
        '''
 
318
        multi fields function method
 
319
        '''
 
320
        # Some verifications
 
321
        if context is None:
 
322
            context = {}
 
323
        if isinstance(ids, (int, long)):
 
324
            ids = [ids]
 
325
            
 
326
        result = {}
 
327
        # date tools object
 
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)
 
331
        
 
332
        for obj in self.browse(cr, uid, ids, context=context):
 
333
            result[obj.id] = {}
 
334
            for f in fields:
 
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})
 
345
                else:
 
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})
 
362
        return result
 
363
    
 
364
    def copy(self, cr, uid, id, default=None, context=None):
 
365
        '''
 
366
        change version name. add (copy)
 
367
        
 
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)
 
371
        '''
 
372
        if default is None:
 
373
            default = {}
 
374
        # state
 
375
        default.update(state='draft')
 
376
        # original reference
 
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'))
 
384
        else:
 
385
            raise osv.except_osv(_('Warning !'), _('Kit Composition List with Batch Number cannot be copied!'))
 
386
            
 
387
        return super(composition_kit, self).copy(cr, uid, id, default, context=context)
 
388
    
 
389
    def unlink(self, cr, uid, ids, context=None):
 
390
        '''
 
391
        cannot delete composition kit not draft
 
392
        '''
 
393
        # Some verifications
 
394
        if context is None:
 
395
            context = {}
 
396
        if isinstance(ids, (int, long)):
 
397
            ids = [ids]
 
398
            
 
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)
 
403
    
 
404
    def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
 
405
        """
 
406
        columns for the tree
 
407
        """
 
408
        if context is None:
 
409
            context = {}
 
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')
 
415
                if view:
 
416
                    view_id = view[1]
 
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')
 
420
                if view:
 
421
                    view_id = view[1]
 
422
                 
 
423
        # call super
 
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
 
432
        
 
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
 
440
        
 
441
        return result
 
442
    
 
443
    def name_get(self, cr, uid, ids, context=None):
 
444
        '''
 
445
        override displayed name
 
446
        '''
 
447
        # Some verifications
 
448
        if context is None:
 
449
            context = {}
 
450
        if isinstance(ids, (int, long)):
 
451
            ids = [ids]
 
452
            
 
453
        # date tools object
 
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)
 
457
        # result
 
458
        res = []
 
459
        
 
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)
 
464
            else:
 
465
                name = obj.composition_combined_ref_lot
 
466
                
 
467
            res += [(obj.id, name)]
 
468
        return res
 
469
    
 
470
    def on_change_product_id(self, cr, uid, ids, product_id, context=None):
 
471
        '''
 
472
        when the product is changed, lot checks are updated - mandatory workaround for attrs use
 
473
        '''
 
474
        # product object
 
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}}
 
484
        if not product_id:
 
485
            return res
 
486
        
 
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']
 
490
        return res
 
491
    
 
492
    def on_change_lot_id(self, cr, uid, ids, lot_id, context=None):
 
493
        '''
 
494
        when the lot is changed, expiry date is updated, so the field is modified before the save happens
 
495
        '''
 
496
        # product object
 
497
        lot_obj = self.pool.get('stock.production.lot')
 
498
        res = {'value': {'composition_exp': False,
 
499
                         'composition_ref_exp': False}}
 
500
        if not lot_id:
 
501
            return res
 
502
            
 
503
        data = lot_obj.read(cr, uid, [lot_id], ['life_date'], context=context)[0]
 
504
        res['value']['composition_exp'] = data['life_date']
 
505
        return res
 
506
    
 
507
    def _get_composition_kit_from_product_ids(self, cr, uid, ids, context=None):
 
508
        '''
 
509
        ids represents the ids of product.product objects for which values have changed
 
510
        
 
511
        return the list of ids of composition.kit objects which need to get their fields updated
 
512
        
 
513
        self is product.product object
 
514
        '''
 
515
        # Some verifications
 
516
        if context is None:
 
517
            context = {}
 
518
        if isinstance(ids, (int, long)):
 
519
            ids = [ids]
 
520
            
 
521
        kit_obj = self.pool.get('composition.kit')
 
522
        result = kit_obj.search(cr, uid, [('composition_product_id', 'in', ids)], context=context)
 
523
        return result
 
524
    
 
525
    def _get_composition_kit_from_lot_ids(self, cr, uid, ids, context=None):
 
526
        '''
 
527
        ids represents the ids of stock.production.lot objects for which values have changed
 
528
        
 
529
        return the list of ids of composition.kit objects which need to get their fields updated
 
530
        
 
531
        self is stock.production.lot object
 
532
        '''
 
533
        # Some verifications
 
534
        if context is None:
 
535
            context = {}
 
536
        if isinstance(ids, (int, long)):
 
537
            ids = [ids]
 
538
            
 
539
        kit_obj = self.pool.get('composition.kit')
 
540
        result = kit_obj.search(cr, uid, [('composition_lot_id', 'in', ids)], context=context)
 
541
        return result
 
542
    
 
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)
 
546
    
 
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)
 
550
    
 
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)
 
554
    
 
555
    def _set_expiry_date(self, cr, uid, ids, field, value, arg, context=None):
 
556
        """
 
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
 
559
        """
 
560
        if not value or field != 'composition_exp':
 
561
            return False
 
562
        if context is None:
 
563
            context = {}
 
564
        if isinstance(ids, (int, long)):
 
565
            ids = [ids]
 
566
        # objects
 
567
        lot_obj = self.pool.get('stock.production.lot')
 
568
        # date tools object
 
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)
 
572
        
 
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))))
 
581
            else:
 
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)
 
584
        return True
 
585
 
 
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),
 
599
                # related
 
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),
 
603
                # functions
 
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),}),
 
613
                # nomenclature
 
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"),
 
654
                }
 
655
    
 
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),
 
663
                 'active': True,
 
664
                 'state': 'draft',
 
665
                 }
 
666
    
 
667
    _order = 'composition_creation_date desc'
 
668
    
 
669
    def _composition_kit_constraint(self, cr, uid, ids, context=None):
 
670
        '''
 
671
        constraint on kit composition - two kits 
 
672
        '''
 
673
        # Some verifications
 
674
        if context is None:
 
675
            context = {}
 
676
        if isinstance(ids, (int, long)):
 
677
            ids = [ids]
 
678
            
 
679
        for obj in self.browse(cr, uid, ids, context=context):
 
680
            # global
 
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)
 
689
                if search_ids:
 
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.'))
 
698
                
 
699
            # real constraints
 
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.'))
 
709
                else:
 
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
 
715
                if not obj.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.'))
 
720
            
 
721
        return True
 
722
    
 
723
    _constraints = [(_composition_kit_constraint, 'Constraint error on Composition Kit.', []),
 
724
                    ]
 
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.'),
 
727
                        ]
 
728
 
 
729
composition_kit()
 
730
 
 
731
 
 
732
class composition_item(osv.osv):
 
733
    '''
 
734
    kit composition items representing kit parts
 
735
    '''
 
736
    _name = 'composition.item'
 
737
    
 
738
    def create(self, cr, uid, vals, context=None):
 
739
        '''
 
740
        force writing of expired_date which is readonly for batch management products
 
741
        '''
 
742
        # objects
 
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
 
758
                    if prod_ids:
 
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})
 
763
                elif perishable:
 
764
                    # nothing special here
 
765
                    pass
 
766
                else:
 
767
                    # not perishable nor management, exp and lot are False
 
768
                    vals.update(item_lot=False, item_exp=False)
 
769
            else:
 
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)
 
773
        
 
774
    def write(self, cr, uid, ids, vals, context=None):
 
775
        '''
 
776
        force writing of expired_date which is readonly for batch management products
 
777
        '''
 
778
        # objects
 
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
 
794
                    if prod_ids:
 
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})
 
799
                elif perishable:
 
800
                    # nothing special here
 
801
                    pass
 
802
                else:
 
803
                    # not perishable nor management, exp and lot are False
 
804
                    vals.update(item_lot=False, item_exp=False)
 
805
            else:
 
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)
 
809
    
 
810
    def on_product_change(self, cr, uid, ids, product_id, context=None):
 
811
        '''
 
812
        product is changed, we update the UoM
 
813
        '''
 
814
        # objects
 
815
        prod_obj = self.pool.get('product.product')
 
816
        result = {'value': {'item_uom_id': False,
 
817
                            'item_qty': 0.0,
 
818
                            'hidden_perishable_mandatory': False,
 
819
                            'hidden_batch_management_mandatory': False,
 
820
                            'item_exp': False,
 
821
                            'item_lot': False,
 
822
                            }}
 
823
        if product_id:
 
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
 
828
            
 
829
        return result
 
830
    
 
831
    def on_lot_change(self, cr, uid, ids, product_id, prodlot_id, context=None):
 
832
        '''
 
833
        if lot exists in the system the date is filled in
 
834
        
 
835
        prodlot_id is the NAME of the production lot
 
836
        '''
 
837
        # objects
 
838
        prod_obj = self.pool.get('product.product')
 
839
        prodlot_obj = self.pool.get('stock.production.lot')
 
840
        # result
 
841
        result = {'value': {}}
 
842
        if product_id:
 
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
 
851
                if prod_ids:
 
852
                    prodlot_id = prod_ids[0]
 
853
                    result['value'].update(item_exp=prodlot_obj.browse(cr, uid, prodlot_id, context=context).life_date)
 
854
                    
 
855
        return result
 
856
    
 
857
    def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
 
858
        """
 
859
        columns for the tree
 
860
        """
 
861
        if context is None:
 
862
            context = {}
 
863
        # call super
 
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
 
872
        
 
873
        return result
 
874
    
 
875
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
876
        '''
 
877
        multi fields function method
 
878
        '''
 
879
        # Some verifications
 
880
        if context is None:
 
881
            context = {}
 
882
        if isinstance(ids, (int, long)):
 
883
            ids = [ids]
 
884
            
 
885
        result = {}
 
886
        for obj in self.browse(cr, uid, ids, context=context):
 
887
            result[obj.id] = {}
 
888
            # name
 
889
            result[obj.id].update({'name': obj.item_product_id.name})
 
890
            # version
 
891
            result[obj.id].update({'item_kit_version': obj.item_kit_id.composition_version})
 
892
            # type
 
893
            result[obj.id].update({'item_kit_type': obj.item_kit_id.composition_type})
 
894
            # state
 
895
            result[obj.id].update({'state': obj.item_kit_id.state})
 
896
            # batch management
 
897
            result[obj.id].update({'hidden_batch_management_mandatory': obj.item_product_id.batch_management})
 
898
            # perishable
 
899
            result[obj.id].update({'hidden_perishable_mandatory': obj.item_product_id.perishable})
 
900
        return result
 
901
    
 
902
    def name_get(self, cr, uid, ids, context=None):
 
903
        '''
 
904
        override displayed name
 
905
        '''
 
906
        # Some verifications
 
907
        if context is None:
 
908
            context = {}
 
909
        if isinstance(ids, (int, long)):
 
910
            ids = [ids]
 
911
            
 
912
        # date tools object
 
913
        date_obj = self.pool.get('date.tools')
 
914
        date_format = date_obj.get_date_format(cr, uid, context=context)
 
915
        # result
 
916
        res = []
 
917
        
 
918
        for obj in self.browse(cr, uid, ids, context=context):
 
919
            name = obj.item_product_id.name
 
920
            res += [(obj.id, name)]
 
921
        return res
 
922
    
 
923
    def _get_composition_item_ids(self, cr, uid, ids, context=None):
 
924
        '''
 
925
        ids represents the ids of composition.kit objects for which values have changed
 
926
        
 
927
        return the list of ids of composition.item objects which need to get their fields updated
 
928
        
 
929
        self is an composition.kit object
 
930
        '''
 
931
        # Some verifications
 
932
        if context is None:
 
933
            context = {}
 
934
        if isinstance(ids, (int, long)):
 
935
            ids = [ids]
 
936
            
 
937
        item_obj = self.pool.get('composition.item')
 
938
        result = item_obj.search(cr, uid, [('item_kit_id', 'in', ids)], context=context)
 
939
        return result
 
940
    
 
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.'),
 
950
                # functions
 
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),
 
964
                }
 
965
    
 
966
    _defaults = {'hidden_batch_management_mandatory': False,
 
967
                 'hidden_perishable_mandatory': False}
 
968
    
 
969
    def _composition_item_constraint(self, cr, uid, ids, context=None):
 
970
        '''
 
971
        constraint on item composition 
 
972
        '''
 
973
        # Some verifications
 
974
        if context is None:
 
975
            context = {}
 
976
        if isinstance(ids, (int, long)):
 
977
            ids = [ids]
 
978
            
 
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
 
982
                if obj.item_lot:
 
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.'))
 
985
                if obj.item_exp:
 
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.'))
 
988
                
 
989
        return True
 
990
    
 
991
    _constraints = [(_composition_item_constraint, 'Constraint error on Composition Item.', []),]
 
992
    
 
993
    
 
994
composition_item()
 
995
 
 
996
 
 
997
class product_product(osv.osv):
 
998
    '''
 
999
    add a constraint - a product of subtype 'kit' cannot be perishable only, should be batch management or nothing
 
1000
    '''
 
1001
    _inherit = 'product.product'
 
1002
    
 
1003
    def _kit_product_constraints(self, cr, uid, ids, context=None):
 
1004
        '''
 
1005
        constraint on product
 
1006
        '''
 
1007
        # Some verifications
 
1008
        if context is None:
 
1009
            context = {}
 
1010
        if isinstance(ids, (int, long)):
 
1011
            ids = [ids]
 
1012
            
 
1013
        for obj in self.browse(cr, uid, ids, context=context):
 
1014
            # kit
 
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.'))
 
1018
            
 
1019
        return True
 
1020
    
 
1021
    _constraints = [(_kit_product_constraints, 'Constraint error on Kit Product.', []),
 
1022
                    ]
 
1023
    
 
1024
product_product()
 
1025
 
 
1026
 
 
1027
class product_nomenclature(osv.osv):
 
1028
    '''
 
1029
    decorator over
 
1030
    
 
1031
    def _getNumberOfProducts(self, cr, uid, ids, field_name, arg, context=None):
 
1032
    '''
 
1033
    _inherit = 'product.nomenclature'
 
1034
    
 
1035
    def _getNumberOfProducts(self, cr, uid, ids, field_name, arg, context=None):
 
1036
        '''
 
1037
        check if we are concerned with composition kit, if we do, we return the number of concerned kit, not product
 
1038
        '''
 
1039
        # Some verifications
 
1040
        if context is None:
 
1041
            context = {}
 
1042
        if isinstance(ids, (int, long)):
 
1043
            ids = [ids]
 
1044
            
 
1045
        if context.get('composition_type', False):
 
1046
            composition_type = context.get('composition_type')
 
1047
            res = {}
 
1048
            for nomen in self.browse(cr, uid, ids, context=context):
 
1049
                name = ''
 
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)
 
1055
                if not kit_ids:
 
1056
                    res[nomen.id] = 0
 
1057
                else:
 
1058
                    res[nomen.id] = len(kit_ids)
 
1059
            return res
 
1060
        else:
 
1061
            return super(product_nomenclature, self)._getNumberOfProducts(cr, uid, ids, field_name, arg, context=context)
 
1062
        
 
1063
    _columns = {'number_of_products': fields.function(_getNumberOfProducts, type='integer', method=True, store=False, string='Number of Products', readonly=True),
 
1064
                }
 
1065
        
 
1066
product_nomenclature()
 
1067
 
 
1068
 
 
1069
class stock_move(osv.osv):
 
1070
    '''
 
1071
    add the new method self.create_composition_list
 
1072
    '''
 
1073
    _inherit= 'stock.move'
 
1074
    
 
1075
    def create_composition_list(self, cr, uid, ids, context=None):
 
1076
        '''
 
1077
        return the form view of composition_list (real) with corresponding values from the context
 
1078
        '''
 
1079
        # Some verifications
 
1080
        if context is None:
 
1081
            context = {}
 
1082
        if isinstance(ids, (int, long)):
 
1083
            ids = [ids]
 
1084
            
 
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
 
1092
        
 
1093
        return {'name': 'Kit Composition List',
 
1094
                'view_id': False,
 
1095
                'view_type': 'form',
 
1096
                'view_mode': 'form,tree',
 
1097
                'res_model': 'composition.kit',
 
1098
                'res_id': False,
 
1099
                'type': 'ir.actions.act_window',
 
1100
                'nodestroy': False,
 
1101
                'target': False,
 
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,
 
1110
                                )
 
1111
                }
 
1112
        
 
1113
    def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
 
1114
        '''
 
1115
        hook to update defaults data
 
1116
        '''
 
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'
 
1122
        
 
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
 
1126
        
 
1127
        kit_id = partial_datas.get('move%s'%(move.id), False).get('composition_list_id')
 
1128
        if kit_id:
 
1129
            defaults.update({'composition_list_id': kit_id})
 
1130
        
 
1131
        return defaults
 
1132
    
 
1133
    _columns = {'composition_list_id': fields.many2one('composition.kit', string='Kit', readonly=True)}
 
1134
 
 
1135
stock_move()
 
1136
 
 
1137
 
 
1138
class stock_location(osv.osv):
 
1139
    '''
 
1140
    add a new reservation method, taking production lot into account
 
1141
    '''
 
1142
    _inherit = 'stock.location'
 
1143
    
 
1144
    def compute_availability(self, cr, uid, ids, consider_child_locations, product_id, uom_id, context=None):
 
1145
        '''
 
1146
        call stock computation function
 
1147
        '''
 
1148
        # Some verifications
 
1149
        if context is None:
 
1150
            context = {}
 
1151
        if isinstance(ids, (int, long)):
 
1152
            ids = [ids]
 
1153
        # objects
 
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)
 
1159
        #print res
 
1160
        return res
 
1161
    
 
1162
    def _product_reserve_lot(self, cr, uid, ids, product_id, uom_id, context=None, lock=False):
 
1163
        """
 
1164
        refactoring of original reserver method, taking production lot into account
 
1165
        
 
1166
        returning the original list-tuple structure + the total qty in each location
 
1167
        """
 
1168
        result = []
 
1169
        amount = 0.0
 
1170
        if context is None:
 
1171
            context = {}
 
1172
        # objects
 
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)
 
1177
        else:
 
1178
            location_ids = ids
 
1179
        # fefo list of lot
 
1180
        fefo_list = []
 
1181
        # data structure
 
1182
        data = {'fefo': fefo_list, 'total': 0.0}
 
1183
            
 
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
 
1188
            if lock:
 
1189
                try:
 
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
 
1197
                                          (
 
1198
                                            (location_dest_id=%s AND
 
1199
                                             location_id<>%s AND
 
1200
                                             state='done')
 
1201
                                            OR
 
1202
                                            (location_id=%s AND
 
1203
                                             location_dest_id<>%s AND
 
1204
                                             state in ('done', 'assigned'))
 
1205
                                          )
 
1206
                                  FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id), log_exceptions=False)
 
1207
                except Exception:
 
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)
 
1215
                    return False
 
1216
 
 
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
 
1221
            cr.execute("""
 
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
 
1224
                                FROM stock_move
 
1225
                                WHERE location_dest_id=%s AND
 
1226
                                location_id<>%s AND
 
1227
                                product_id=%s AND
 
1228
                                state='done'
 
1229
                                GROUP BY product_uom, prodlot_id, expired_date
 
1230
                            
 
1231
                                UNION
 
1232
                            
 
1233
                                SELECT product_uom, prodlot_id, expired_date, -sum(product_qty) AS product_qty
 
1234
                                FROM stock_move
 
1235
                                WHERE location_id=%s AND
 
1236
                                location_dest_id<>%s AND
 
1237
                                product_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
 
1242
                       """,
 
1243
                       (id, id, product_id, id, id, product_id))
 
1244
            results = cr.dictfetchall()
 
1245
            # merge results according to uom if needed
 
1246
            for r in results:
 
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)
 
1251
                total += amount
 
1252
                data.update({'total': total})
 
1253
                # fill the data structure, total value for location
 
1254
                loc_tot = data.setdefault(id, {}).setdefault('total', 0.0)
 
1255
                loc_tot += amount
 
1256
                data.setdefault(id, {}).update({'total': loc_tot})
 
1257
                # production lot
 
1258
                lot_tot = data.setdefault(id, {}).setdefault(r['prodlot_id'], {}).setdefault('total', 0.0)
 
1259
                lot_tot += amount
 
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
 
1263
                if r['prodlot_id']:
 
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
 
1267
                        if lot_tot > 0:
 
1268
                            fefo_list[-1].update({'qty': lot_tot})
 
1269
                        else:
 
1270
                            fefo_list.pop(-1)
 
1271
                    elif lot_tot > 0:
 
1272
                        # append a new dic
 
1273
                        fefo_list.append({'location_id': id,
 
1274
                                          'uom_id': uom_id,
 
1275
                                          'expired_date': r['expired_date'],
 
1276
                                          'prodlot_id': r['prodlot_id'],
 
1277
                                          'product_id': product_id,
 
1278
                                          'qty': lot_tot})
 
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)
 
1281
        return data
 
1282
    
 
1283
stock_location()
 
1284
 
 
1285
 
 
1286
class stock_picking(osv.osv):
 
1287
    '''
 
1288
    treat the composition list
 
1289
    '''
 
1290
    _inherit = 'stock.picking'
 
1291
 
 
1292
    def _do_partial_hook(self, cr, uid, ids, context, *args, **kwargs):
 
1293
        '''
 
1294
        hook to update defaults data
 
1295
        '''
 
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'
 
1301
        
 
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')
 
1305
        if kit_id:
 
1306
            defaults.update({'composition_list_id': kit_id})
 
1307
        
 
1308
        return defaults
 
1309
 
 
1310
stock_picking()
 
1311
 
 
1312
 
 
1313
class purchase_order_line(osv.osv):
 
1314
    '''
 
1315
    add theoretical de-kitting capabilities
 
1316
    '''
 
1317
    _inherit = 'purchase.order.line'
 
1318
    
 
1319
    def de_kitting(self, cr, uid, ids, context=None):
 
1320
        '''
 
1321
        open theoretical kit selection
 
1322
        '''
 
1323
        if context is None:
 
1324
            context = {}
 
1325
        # data
 
1326
        name = _("Replacement Items Selection")
 
1327
        model = 'kit.selection'
 
1328
        step = 'default'
 
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))
 
1339
        return res
 
1340
 
 
1341
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
1342
        '''
 
1343
        multi fields function method
 
1344
        '''
 
1345
        # Some verifications
 
1346
        if context is None:
 
1347
            context = {}
 
1348
        if isinstance(ids, (int, long)):
 
1349
            ids = [ids]
 
1350
        # objects
 
1351
        kit_obj = self.pool.get('composition.kit')
 
1352
        result = {}
 
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)
 
1362
#                if kit_ids:
 
1363
#                    result[obj.id].update({'kit_pol_check': True})
 
1364
        return result
 
1365
    
 
1366
    _columns = {'kit_pol_check' : fields.function(_vals_get, method=True, string='Kit Mem Check', type='boolean', readonly=True, multi='get_vals_kit'),
 
1367
                }
 
1368
 
 
1369
purchase_order_line()