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

« back to all changes in this revision

Viewing changes to msf_outgoing/msf_outgoing.py

  • Committer: Olivier DOSSMANN
  • Date: 2013-05-31 14:22:09 UTC
  • mto: This revision was merged to the branch mainline in revision 1687.
  • Revision ID: od@tempo-consulting.fr-20130531142209-sbcwvzuema11guzz
UF-1991 [FIX] Problem with wizard on "msg" field. Change it to "name".

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, date
 
26
 
 
27
from dateutil.relativedelta import relativedelta
 
28
import decimal_precision as dp
 
29
import logging
 
30
import tools
 
31
import time
 
32
from os import path
 
33
 
 
34
class stock_warehouse(osv.osv):
 
35
    '''
 
36
    add new packing, dispatch and distribution locations for input
 
37
    '''
 
38
    _inherit = "stock.warehouse"
 
39
    _name = "stock.warehouse"
 
40
    
 
41
    _columns = {'lot_packing_id': fields.many2one('stock.location', 'Location Packing', required=True, domain=[('usage','<>','view')]),
 
42
                'lot_dispatch_id': fields.many2one('stock.location', 'Location Dispatch', required=True, domain=[('usage','<>','view')]),
 
43
                'lot_distribution_id': fields.many2one('stock.location', 'Location Distribution', required=True, domain=[('usage','<>','view')]),
 
44
                }
 
45
    
 
46
    _defaults = {'lot_packing_id': lambda obj, cr, uid, c: len(obj.pool.get('stock.location').search(cr, uid, [('name', '=', 'Packing'),], context=c)) and obj.pool.get('stock.location').search(cr, uid, [('name', '=', 'Packing'),], context=c)[0] or False,
 
47
                 'lot_dispatch_id': lambda obj, cr, uid, c: len(obj.pool.get('stock.location').search(cr, uid, [('name', '=', 'Dispatch'),], context=c)) and obj.pool.get('stock.location').search(cr, uid, [('name', '=', 'Dispatch'),], context=c)[0] or False,
 
48
                 'lot_distribution_id': lambda obj, cr, uid, c: len(obj.pool.get('stock.location').search(cr, uid, [('name', '=', 'Distribution'),], context=c)) and obj.pool.get('stock.location').search(cr, uid, [('name', '=', 'Distribution'),], context=c)[0] or False,
 
49
                }
 
50
 
 
51
stock_warehouse()
 
52
 
 
53
 
 
54
class pack_type(osv.osv):
 
55
    '''
 
56
    pack type corresponding to a type of pack (name, length, width, height)
 
57
    '''
 
58
    _name = 'pack.type'
 
59
    _description = 'Pack Type'
 
60
    _columns = {'name': fields.char(string='Name', size=1024),
 
61
                'length': fields.float(digits=(16,2), string='Length [cm]'),
 
62
                'width': fields.float(digits=(16,2), string='Width [cm]'),
 
63
                'height': fields.float(digits=(16,2), string='Height [cm]'),
 
64
                }
 
65
 
 
66
pack_type()
 
67
 
 
68
 
 
69
class shipment(osv.osv):
 
70
    '''
 
71
    a shipment presents the data from grouped stock moves in a 'sequence' way
 
72
    '''
 
73
    _name = 'shipment'
 
74
    _description = 'represents a group of pack families'
 
75
    
 
76
    def copy(self, cr, uid, id, default=None, context=None):
 
77
        '''
 
78
        prevent copy
 
79
        '''
 
80
        raise osv.except_osv(_('Error !'), _('Shipment copy is forbidden.'))
 
81
    
 
82
    def copy_data(self, cr, uid, id, default=None, context=None):
 
83
        '''
 
84
        reset one2many fields
 
85
        '''
 
86
        if default is None:
 
87
            default = {}
 
88
        if context is None:
 
89
            context = {}
 
90
        # reset one2many fields
 
91
        default.update(pack_family_memory_ids=[])
 
92
        result = super(shipment, self).copy_data(cr, uid, id, default=default, context=context)
 
93
        
 
94
        return result
 
95
    
 
96
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
97
        '''
 
98
        multi function for global shipment values
 
99
        '''
 
100
        pf_memory_obj = self.pool.get('pack.family.memory')
 
101
        picking_obj = self.pool.get('stock.picking')
 
102
        
 
103
        result = {}
 
104
        for shipment in self.browse(cr, uid, ids, context=context):
 
105
            values = {'total_amount': 0.0,
 
106
                      'currency_id': False,
 
107
                      'num_of_packs': 0,
 
108
                      'total_weight': 0.0,
 
109
                      'total_volume': 0.0,
 
110
                      'state': 'draft',
 
111
                      'backshipment_id': False,
 
112
                      }
 
113
            result[shipment.id] = values
 
114
            # gather the state from packing objects, all packing must have the same state for shipment
 
115
            # for draft shipment, we can have done packing and draft packing
 
116
            packing_ids = picking_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
 
117
            # fields to check and get
 
118
            state = None
 
119
            first_shipment_packing_id = None
 
120
            backshipment_id = None
 
121
            # delivery validated
 
122
            delivery_validated = None
 
123
            # browse the corresponding packings
 
124
            for packing in picking_obj.browse(cr, uid, packing_ids, context=context):
 
125
                # state check
 
126
                # because when the packings are validated one after the other, it triggers the compute of state, and if we have multiple packing for this shipment, it will fail
 
127
                # if one packing is draft, even if other packing have been shipped, the shipment must stay draft until all packing are done
 
128
                if state != 'draft':
 
129
                    state = packing.state
 
130
                    
 
131
                # all corresponding shipment must be dev validated or not
 
132
                if packing.delivered:
 
133
                    # integrity check
 
134
                    if delivery_validated is not None and delivery_validated != packing.delivered:
 
135
                        # two packing have different delivery validated values -> problem
 
136
                        assert False, 'All packing do not have the same validated value - %s - %s'%(delivery_validated, packing.delivered)
 
137
                    # update the value
 
138
                    delivery_validated = packing.delivered
 
139
                
 
140
                # first_shipment_packing_id check - no check for the same reason
 
141
                first_shipment_packing_id = packing.first_shipment_packing_id.id
 
142
                
 
143
                # backshipment_id check
 
144
                if backshipment_id and backshipment_id != packing.backorder_id.shipment_id.id:
 
145
                    assert False, 'all packing of the shipment have not the same draft shipment correspondance - %s - %s'%(backshipment_id, packing.backorder_id.shipment_id.id)
 
146
                backshipment_id = packing.backorder_id and packing.backorder_id.shipment_id.id or False
 
147
            
 
148
            # if state is in ('draft', 'done', 'cancel'), the shipment keeps the same state
 
149
            if state not in ('draft', 'done', 'cancel',):
 
150
                if first_shipment_packing_id:
 
151
                    # second step of shipment : shipped
 
152
                    state = 'shipped'
 
153
                else:
 
154
                    state = 'packed'
 
155
            elif state == 'done':
 
156
                if delivery_validated:
 
157
                    # special state corresponding to delivery validated
 
158
                    state = 'delivered'
 
159
                    
 
160
            values['state'] = state
 
161
            values['backshipment_id'] = backshipment_id
 
162
            
 
163
            for memory_family in shipment.pack_family_memory_ids:
 
164
                # taken only into account if not done (done means returned packs)
 
165
                if shipment.state in ('delivered', 'done') or memory_family.state not in ('done',) :
 
166
                    # num of packs
 
167
                    num_of_packs = memory_family.num_of_packs
 
168
                    values['num_of_packs'] += int(num_of_packs)
 
169
                    # total weight
 
170
                    total_weight = memory_family.total_weight
 
171
                    values['total_weight'] += int(total_weight)
 
172
                    # total volume
 
173
                    total_volume = memory_family.total_volume
 
174
                    values['total_volume'] += float(total_volume)
 
175
                    # total amount
 
176
                    total_amount = memory_family.total_amount
 
177
                    values['total_amount'] += total_amount
 
178
                    # currency
 
179
                    currency_id = memory_family.currency_id and memory_family.currency_id.id or False
 
180
                    values['currency_id'] = currency_id
 
181
                
 
182
        return result
 
183
    
 
184
    def _get_shipment_ids(self, cr, uid, ids, context=None):
 
185
        '''
 
186
        ids represents the ids of stock.picking objects for which state has changed
 
187
        
 
188
        return the list of ids of shipment object which need to get their state field updated
 
189
        '''
 
190
        pack_obj = self.pool.get('stock.picking')
 
191
        result = []
 
192
        for packing in pack_obj.browse(cr, uid, ids, context=context):
 
193
            if packing.shipment_id and packing.shipment_id.id not in result:
 
194
                result.append(packing.shipment_id.id)
 
195
        return result
 
196
    
 
197
    def _packs_search(self, cr, uid, obj, name, args, context=None):
 
198
        """ 
 
199
        Searches Ids of shipment
 
200
        """
 
201
        if context is None:
 
202
            context = {}
 
203
            
 
204
        shipments = self.pool.get('shipment').search(cr, uid, [], context=context)
 
205
        # result dic
 
206
        result = {}
 
207
        for shipment in self.browse(cr, uid, shipments, context=context):
 
208
            result[shipment.id] = shipment.num_of_packs
 
209
        # construct the request
 
210
        # adapt the operator
 
211
        op = args[0][1]
 
212
        if op == '=':
 
213
            op = '=='
 
214
        ids = [('id', 'in', [x for x in result.keys() if eval("%s %s %s"%(result[x], op, args[0][2]))])]
 
215
        return ids
 
216
 
 
217
    _columns = {'name': fields.char(string='Reference', size=1024),
 
218
                'date': fields.datetime(string='Creation Date'),
 
219
                'shipment_expected_date': fields.datetime(string='Expected Ship Date'),
 
220
                'shipment_actual_date': fields.datetime(string='Actual Ship Date', readonly=True,),
 
221
                'transport_type': fields.selection([('by_road', 'By road')],
 
222
                                                   string="Transport Type", readonly=True),
 
223
                'address_id': fields.many2one('res.partner.address', 'Address', help="Address of customer"),
 
224
                'sequence_id': fields.many2one('ir.sequence', 'Shipment Sequence', help="This field contains the information related to the numbering of the shipment.", ondelete='cascade'),
 
225
                # cargo manifest things
 
226
                'cargo_manifest_reference': fields.char(string='Cargo Manifest Reference', size=1024,),
 
227
                'date_of_departure': fields.date(string='Date of Departure'),
 
228
                'planned_date_of_arrival': fields.date(string='Planned Date of Arrival'),
 
229
                'transit_via': fields.char(string='Transit via', size=1024),
 
230
                'registration': fields.char(string='Registration', size=1024),
 
231
                'driver_name': fields.char(string='Driver Name', size=1024),
 
232
                # -- shipper
 
233
                'shipper_name': fields.char(string='Name', size=1024),
 
234
                'shipper_address': fields.char(string='Address', size=1024),
 
235
                'shipper_phone': fields.char(string='Phone', size=1024),
 
236
                'shipper_email': fields.char(string='Email', size=1024),
 
237
                'shipper_other': fields.char(string='Other', size=1024),
 
238
                'shipper_date': fields.date(string='Date'),
 
239
                'shipper_signature': fields.char(string='Signature', size=1024),
 
240
                # -- carrier
 
241
                'carrier_name': fields.char(string='Name', size=1024),
 
242
                'carrier_address': fields.char(string='Address', size=1024),
 
243
                'carrier_phone': fields.char(string='Phone', size=1024),
 
244
                'carrier_email': fields.char(string='Email', size=1024),
 
245
                'carrier_other': fields.char(string='Other', size=1024),
 
246
                'carrier_date': fields.date(string='Date'),
 
247
                'carrier_signature': fields.char(string='Signature', size=1024),
 
248
                # -- consignee
 
249
                'consignee_name': fields.char(string='Name', size=1024),
 
250
                'consignee_address': fields.char(string='Address', size=1024),
 
251
                'consignee_phone': fields.char(string='Phone', size=1024),
 
252
                'consignee_email': fields.char(string='Email', size=1024),
 
253
                'consignee_other': fields.char(string='Other', size=1024),
 
254
                'consignee_date': fields.date(string='Date'),
 
255
                'consignee_signature': fields.char(string='Signature', size=1024),
 
256
                # functions
 
257
                'partner_id': fields.related('address_id', 'partner_id', type='many2one', relation='res.partner', string='Customer', store=True),
 
258
                'partner_id2': fields.many2one('res.partner', string='Customer', required=False),
 
259
                'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', multi='get_vals',),
 
260
                'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
 
261
                'num_of_packs': fields.function(_vals_get, method=True, fnct_search=_packs_search, type='integer', string='Number of Packs', multi='get_vals_X',), # old_multi ship_vals
 
262
                'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals',),
 
263
                'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals',),
 
264
                'state': fields.function(_vals_get, method=True, type='selection', selection=[('draft', 'Draft'),
 
265
                                                                                              ('packed', 'Packed'),
 
266
                                                                                              ('shipped', 'Shipped'),
 
267
                                                                                              ('done', 'Closed'),
 
268
                                                                                              ('delivered', 'Delivered'),
 
269
                                                                                              ('cancel', 'Cancelled')], string='State', multi='get_vals',
 
270
                                         store= {'stock.picking': (_get_shipment_ids, ['state', 'shipment_id', 'delivered'], 10),}),
 
271
                'backshipment_id': fields.function(_vals_get, method=True, type='many2one', relation='shipment', string='Draft Shipment', multi='get_vals',),
 
272
                # added by Quentin https://bazaar.launchpad.net/~unifield-team/unifield-wm/trunk/revision/426.20.14
 
273
                'parent_id': fields.many2one('shipment', string='Parent shipment'),
 
274
                'invoice_id': fields.many2one('account.invoice', string='Related invoice'),
 
275
                }
 
276
    _defaults = {'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),}
 
277
    
 
278
    
 
279
    _order = 'name desc'
 
280
    
 
281
    def create_shipment(self, cr, uid, ids, context=None):
 
282
        '''
 
283
        open the wizard to create (partial) shipment
 
284
        '''
 
285
        # we need the context for the wizard switch
 
286
        if context is None:
 
287
            context = {}
 
288
        context['group_by'] = False
 
289
            
 
290
        wiz_obj = self.pool.get('wizard')
 
291
        
 
292
        # data
 
293
        name = _("Create Shipment")
 
294
        model = 'shipment.wizard'
 
295
        step = 'create'
 
296
        # open the selected wizard
 
297
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
298
    
 
299
    def do_create_shipment(self, cr, uid, ids, context=None):
 
300
        '''
 
301
        for each original draft picking:
 
302
         - creation of the new packing object with empty moves
 
303
         - convert partial data to move related data
 
304
         - create corresponding moves in new packing
 
305
         - update initial packing object
 
306
         - trigger workflow for new packing object
 
307
        '''
 
308
        # integrity check
 
309
        assert context, 'no context, method call is wrong'
 
310
        assert 'partial_datas_shipment' in context, 'partial_datas_shipment no defined in context'
 
311
        
 
312
        pick_obj = self.pool.get('stock.picking')
 
313
        move_obj = self.pool.get('stock.move')
 
314
        shipment_obj = self.pool.get('shipment')
 
315
        
 
316
        # data from wizard
 
317
        partial_datas_shipment = context['partial_datas_shipment']
 
318
        # shipment ids from ids must be equal to shipment ids from partial datas
 
319
        assert set(ids) == set(partial_datas_shipment.keys()), 'shipment ids from ids and partial do not match'
 
320
        
 
321
        for draft_shipment in self.browse(cr, uid, partial_datas_shipment.keys(), context=context):
 
322
            # for each shipment create a new shipment which will be used by the group of new packing objects
 
323
            address_id = shipment_obj.read(cr, uid, [draft_shipment.id], ['address_id'], context=context)[0]['address_id'][0]
 
324
            partner_id = shipment_obj.read(cr, uid, [draft_shipment.id], ['partner_id'], context=context)[0]['partner_id'][0]
 
325
            sequence = draft_shipment.sequence_id
 
326
            shipment_number = sequence.get_id(test='id', context=context)
 
327
            # state is a function - not set
 
328
            shipment_name = draft_shipment.name + '-' + shipment_number
 
329
            # 
 
330
            values = {'name': shipment_name, 'address_id': address_id, 'partner_id': partner_id, 'partner_id2': partner_id, 'shipment_expected_date': draft_shipment.shipment_expected_date, 'shipment_actual_date': draft_shipment.shipment_actual_date, 'parent_id': draft_shipment.id}
 
331
            shipment_id = shipment_obj.create(cr, uid, values, context=context)
 
332
            context['shipment_id'] = shipment_id
 
333
            for draft_packing in pick_obj.browse(cr, uid, partial_datas_shipment[draft_shipment.id].keys(), context=context):
 
334
                # copy the picking object without moves
 
335
                # creation of moves and update of initial in picking create method
 
336
                context.update(draft_shipment_id=draft_shipment.id, draft_packing_id=draft_packing.id)
 
337
                sequence = draft_packing.sequence_id
 
338
                packing_number = sequence.get_id(test='id', context=context)
 
339
                new_packing_id = pick_obj.copy(cr, uid, draft_packing.id,
 
340
                                               {'name': draft_packing.name + '-' + packing_number,
 
341
                                                'backorder_id': draft_packing.id,
 
342
                                                'shipment_id': False,
 
343
                                                'move_lines': []}, context=dict(context, keep_prodlot=True, allow_copy=True, non_stock_noupdate=True))
 
344
 
 
345
                # confirm the new packing
 
346
                wf_service = netsvc.LocalService("workflow")
 
347
                wf_service.trg_validate(uid, 'stock.picking', new_packing_id, 'button_confirm', cr)
 
348
                # simulate check assign button, as stock move must be available
 
349
                pick_obj.force_assign(cr, uid, [new_packing_id])
 
350
                
 
351
            # log creation message
 
352
            self.log(cr, uid, shipment_id, _('The new Shipment %s has been created.')%(shipment_name,))
 
353
            # the shipment is automatically shipped, no more pack states in between.
 
354
            self.ship(cr, uid, [shipment_id], context=context)
 
355
 
 
356
        # TODO which behavior
 
357
        #return {'type': 'ir.actions.act_window_close'}
 
358
        data_obj = self.pool.get('ir.model.data')
 
359
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
 
360
        view_id = view_id and view_id[1] or False
 
361
        return {
 
362
            'name':_("Shipment"),
 
363
            'view_mode': 'form,tree',
 
364
            'view_id': [view_id],
 
365
            'view_type': 'form',
 
366
            'res_model': 'shipment',
 
367
            'res_id': shipment_id,
 
368
            'type': 'ir.actions.act_window',
 
369
            'target': 'crush',
 
370
        }
 
371
    
 
372
    def return_packs(self, cr, uid, ids, context=None):
 
373
        '''
 
374
        open the wizard to return packs from draft shipment
 
375
        '''
 
376
        # we need the context for the wizard switch
 
377
        if context is None:
 
378
            context = {}
 
379
            
 
380
        wiz_obj = self.pool.get('wizard')
 
381
        
 
382
        # data
 
383
        name = _("Return Packs")
 
384
        model = 'shipment.wizard'
 
385
        step = 'returnpacks'
 
386
        # open the selected wizard
 
387
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
388
    
 
389
    def do_return_packs(self, cr, uid, ids, context=None):
 
390
        '''
 
391
        for each original draft picking:
 
392
         - convert partial data to move related data
 
393
         - update the draft_packing's moves, decrease quantity and update from/to info
 
394
         - update initial packing object
 
395
         - create a back move for each move with return quantity to initial location
 
396
         - increase quantity of related draft_picking_ticket's moves
 
397
        '''
 
398
        # integrity check
 
399
        assert context, 'no context, method call is wrong'
 
400
        assert 'partial_datas' in context, 'partial_datas no defined in context'
 
401
        
 
402
        pick_obj = self.pool.get('stock.picking')
 
403
        move_obj = self.pool.get('stock.move')
 
404
        obj_data = self.pool.get('ir.model.data')
 
405
        
 
406
        # data from wizard
 
407
        partial_datas = context['partial_datas']
 
408
        # shipment ids from ids must be equal to shipment ids from partial datas
 
409
        assert set(ids) == set(partial_datas.keys()), 'shipment ids from ids and partial do not match'
 
410
       
 
411
        draft_picking_id = False
 
412
        for draft_shipment_id in partial_datas:
 
413
            # log flag - log for draft shipment is displayed only one time for each draft shipment
 
414
            log_flag = False
 
415
            # for each draft packing
 
416
            for draft_packing in pick_obj.browse(cr, uid, partial_datas[draft_shipment_id].keys(), context=context):
 
417
                # corresponding draft picking ticket -> draft_packing - ppl - picking_ticket - draft_picking_ticket
 
418
                draft_picking = draft_packing.previous_step_id.previous_step_id.backorder_id
 
419
                draft_picking_id = draft_packing.previous_step_id.previous_step_id.backorder_id.id
 
420
                # for each sequence
 
421
                for from_pack in partial_datas[draft_shipment_id][draft_packing.id]:
 
422
                    for to_pack in partial_datas[draft_shipment_id][draft_packing.id][from_pack]:
 
423
                        # partial data for one sequence of one draft packing
 
424
                        data = partial_datas[draft_shipment_id][draft_packing.id][from_pack][to_pack][0]
 
425
                        # total number of packs
 
426
                        total_num = to_pack - from_pack + 1
 
427
                        # number of returned packs
 
428
                        selected_number = data['selected_number']
 
429
                        # we take the packs with the highest numbers
 
430
                        # new moves
 
431
                        selected_from_pack = to_pack - selected_number + 1
 
432
                        selected_to_pack = to_pack
 
433
                        # update initial moves
 
434
                        if selected_number == total_num:
 
435
                            # if all packs have been selected, from/to are set to 0
 
436
                            initial_from_pack = 0
 
437
                            initial_to_pack = 0
 
438
                        else:
 
439
                            initial_from_pack = from_pack
 
440
                            initial_to_pack = to_pack - selected_number
 
441
                        # find the concerned stock moves
 
442
                        move_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_packing.id),
 
443
                                                             ('from_pack', '=', from_pack),
 
444
                                                             ('to_pack', '=', to_pack)])
 
445
                        # update the moves, decrease the quantities
 
446
                        for move in move_obj.browse(cr, uid, move_ids, context=context):
 
447
                            # stock move are not canceled as for ppl return process
 
448
                            # because this represents a draft packing, meaning some shipment could be canceled and
 
449
                            # returned to this stock move
 
450
                            # initial quantity
 
451
                            initial_qty = move.product_qty
 
452
                            # quantity to return
 
453
                            return_qty = selected_number * move.qty_per_pack
 
454
                            # update initial quantity
 
455
                            initial_qty = max(initial_qty - return_qty, 0)
 
456
                            values = {'product_qty': initial_qty,
 
457
                                      'from_pack': initial_from_pack,
 
458
                                      'to_pack': initial_to_pack,}
 
459
                            
 
460
                            move_obj.write(cr, uid, [move.id], values, context=context)
 
461
                            
 
462
                            # create a back move with the quantity to return to the good location
 
463
                            # the good location is stored in the 'initial_location' field
 
464
                            copy_id = move_obj.copy(cr, uid, move.id, {'product_qty': return_qty,
 
465
                                                             'location_dest_id': move.initial_location.id,
 
466
                                                             'from_pack': selected_from_pack,
 
467
                                                             'to_pack': selected_to_pack,
 
468
                                                             'state': 'done'}, context=dict(context, non_stock_noupdate=True))
 
469
                            # find the corresponding move in draft in the draft **picking**
 
470
                            draft_move = move.backmove_id
 
471
                            # increase the draft move with the move quantity
 
472
                            draft_initial_qty = move_obj.read(cr, uid, [draft_move.id], ['product_qty'], context=context)[0]['product_qty']
 
473
                            draft_initial_qty += return_qty
 
474
                            move_obj.write(cr, uid, [draft_move.id], {'product_qty': draft_initial_qty}, context=context)
 
475
            
 
476
                # log the increase action - display the picking ticket view form - log message for each draft packing because each corresponds to a different draft picking
 
477
                if not log_flag:
 
478
                    draft_shipment_name = self.read(cr, uid, draft_shipment_id, ['name'], context=context)['name']
 
479
                    self.log(cr, uid, draft_shipment_id, _("Packs from the draft Shipment (%s) have been returned to stock.")%(draft_shipment_name,))
 
480
                    log_flag = True
 
481
                res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
 
482
                self.pool.get('stock.picking').log(cr, uid, draft_picking_id, _("The corresponding Draft Picking Ticket (%s) has been updated.")%(draft_picking.name,), context={'view_id': res,})
 
483
            
 
484
        # call complete_finished on the shipment object
 
485
        # if everything is alright (all draft packing are finished) the shipment is done also 
 
486
        result = self.complete_finished(cr, uid, partial_datas.keys(), context=context)
 
487
        
 
488
        # TODO which behavior
 
489
        #return {'type': 'ir.actions.act_window_close'}
 
490
        data_obj = self.pool.get('ir.model.data')
 
491
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
 
492
        view_id = view_id and view_id[1] or False
 
493
        return {
 
494
            'name':_("Picking Ticket"),
 
495
            'view_mode': 'form,tree',
 
496
            'view_id': [view_id],
 
497
            'view_type': 'form',
 
498
            'res_model': 'stock.picking',
 
499
            'res_id': draft_picking_id ,
 
500
            'type': 'ir.actions.act_window',
 
501
            'target': 'crush',
 
502
        }
 
503
    
 
504
    def return_packs_from_shipment(self, cr, uid, ids, context=None):
 
505
        '''
 
506
        open the wizard to return packs from draft shipment
 
507
        '''
 
508
        # we need the context for the wizard switch
 
509
        if context is None:
 
510
            context = {}
 
511
            
 
512
        wiz_obj = self.pool.get('wizard')
 
513
        
 
514
        # data
 
515
        name = _("Return Packs from Shipment")
 
516
        model = 'shipment.wizard'
 
517
        step = 'returnpacksfromshipment'
 
518
        # open the selected wizard
 
519
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
520
    
 
521
    def compute_sequences(self, cr, uid, ids, context=None, *args, **kwargs):
 
522
        '''
 
523
        compute corresponding sequences
 
524
        '''
 
525
        datas = kwargs['datas']
 
526
        from_pack = kwargs['from_pack']
 
527
        to_pack = kwargs['to_pack']
 
528
        # the list of tuple representing the packing movements from/to - default to sequence value
 
529
        stay = [(from_pack, to_pack)]
 
530
        # the list of tuple representing the draft packing movements from/to
 
531
        back_to_draft = []
 
532
        
 
533
        # loop the partials
 
534
        for partial in datas:
 
535
            return_from = partial['return_from']
 
536
            return_to = partial['return_to']
 
537
            # create the corresponding tuple
 
538
            back_to_draft.append((return_from, return_to))
 
539
            # stay list must be ordered
 
540
            sorted(stay)
 
541
            # find the corresponding tuple in the stay list
 
542
            for i in range(len(stay)):
 
543
                # the tuple are ordered
 
544
                seq = stay[i]
 
545
                if seq[1] >= return_to:
 
546
                    # this is the good tuple
 
547
                    # stay tuple creation logic
 
548
                    if return_from == seq[0]:
 
549
                        if return_to == seq[1]:
 
550
                            # all packs for this sequence are sent back - simply remove it
 
551
                            break
 
552
                        else:
 
553
                            # to+1-seq[1] in stay
 
554
                            stay.append((return_to+1, seq[1]))
 
555
                            break
 
556
                    
 
557
                    elif return_to == seq[1]:
 
558
                        # do not start at beginning, but same end
 
559
                        stay.append((seq[0], return_from-1))
 
560
                        break
 
561
                    
 
562
                    else:
 
563
                        # in the middle, two new tuple in stay
 
564
                        stay.append((seq[0], return_from-1))
 
565
                        stay.append((return_to+1, seq[1]))
 
566
                        break
 
567
            
 
568
            # old one is always removed
 
569
            stay.pop(i)
 
570
            
 
571
        # return both values - return order is important
 
572
        return stay, back_to_draft
 
573
    
 
574
    def do_return_packs_from_shipment(self, cr, uid, ids, context=None):
 
575
        '''
 
576
        return the packs to the corresponding draft packing object
 
577
        
 
578
        for each corresponding draft packing
 
579
        - 
 
580
        '''
 
581
        # integrity check
 
582
        assert context, 'no context, method call is wrong'
 
583
        assert 'partial_datas' in context, 'partial_datas no defined in context'
 
584
        
 
585
        pick_obj = self.pool.get('stock.picking')
 
586
        move_obj = self.pool.get('stock.move')
 
587
        wf_service = netsvc.LocalService("workflow")
 
588
        
 
589
        # data from wizard
 
590
        partial_datas = context['partial_datas']
 
591
        # shipment ids from ids must be equal to shipment ids from partial datas
 
592
        assert set(ids) == set(partial_datas.keys()), 'shipment ids from ids and partial do not match'
 
593
        
 
594
        dispatch_name = _('Dispatch')
 
595
        
 
596
        # for each shipment
 
597
        for shipment_id in partial_datas:
 
598
            # for each packing
 
599
            for packing in pick_obj.browse(cr, uid, partial_datas[shipment_id].keys(), context=context):
 
600
                # corresponding draft packing -> backorder
 
601
                draft_packing_id = packing.backorder_id.id
 
602
                # corresponding draft shipment (all packing for a shipment belong to the same draft_shipment)
 
603
                draft_shipment_id = packing.backorder_id.shipment_id.id
 
604
                # for each sequence
 
605
                for from_pack in partial_datas[shipment_id][packing.id]:
 
606
                    for to_pack in partial_datas[shipment_id][packing.id][from_pack]:
 
607
                        # partial datas for one sequence of one packing
 
608
                        # could have multiple data multiple products in the same pack family
 
609
                        datas = partial_datas[shipment_id][packing.id][from_pack][to_pack]
 
610
                        # the corresponding moves
 
611
                        move_ids = move_obj.search(cr, uid, [('picking_id', '=', packing.id),
 
612
                                                             ('from_pack', '=', from_pack),
 
613
                                                             ('to_pack', '=', to_pack)], context=context)
 
614
                        
 
615
                        # compute the sequences to stay/to return to draft packing
 
616
                        stay, back_to_draft = self.compute_sequences(cr, uid, ids, context=context,
 
617
                                                                     datas=datas,
 
618
                                                                     from_pack=from_pack,
 
619
                                                                     to_pack=to_pack,)
 
620
                        
 
621
                        # we have the information concerning movements to update the packing and the draft packing
 
622
                        
 
623
                        # update the packing object, we update the existing move
 
624
                        # if needed new moves are created
 
625
                        updated = {}
 
626
                        for move in move_obj.browse(cr, uid, move_ids, context=context):
 
627
                            # update values
 
628
                            updated[move.id] = {'initial': move.product_qty, 'partial_qty': 0}
 
629
                            # loop through stay sequences
 
630
                            for seq in stay:
 
631
                                # corresponding number of packs
 
632
                                selected_number = seq[1] - seq[0] + 1
 
633
                                # quantity to return
 
634
                                new_qty = selected_number * move.qty_per_pack
 
635
                                # for both cases, we update the from/to and compute the corresponding quantity
 
636
                                # if the move has been updated already, we copy/update
 
637
                                values = {'from_pack': seq[0],
 
638
                                          'to_pack': seq[1],
 
639
                                          'product_qty': new_qty,
 
640
                                          'state': 'assigned'}
 
641
                                
 
642
                                # the original move is never modified, but canceled
 
643
                                updated[move.id]['partial_qty'] += new_qty
 
644
                                new_move_id = move_obj.copy(cr, uid, move.id, values, context=context)
 
645
                                
 
646
#                            # nothing stays
 
647
#                            if 'partial_qty' not in updated[move.id]:
 
648
#                                updated[move.id]['partial_qty'] = 0
 
649
                                    
 
650
                            # loop through back_to_draft sequences
 
651
                            for seq in back_to_draft:
 
652
                                # for each sequence we add the corresponding stock move to draft packing
 
653
                                # corresponding number of packs
 
654
                                selected_number = seq[1] - seq[0] + 1
 
655
                                # quantity to return
 
656
                                new_qty = selected_number * move.qty_per_pack
 
657
                                # values
 
658
                                location_dispatch = move.picking_id.warehouse_id.lot_dispatch_id.id
 
659
                                location_distrib = move.picking_id.warehouse_id.lot_distribution_id.id
 
660
                                dispatch_name = move.picking_id.warehouse_id.lot_dispatch_id.name
 
661
                                values = {'from_pack': seq[0],
 
662
                                          'to_pack': seq[1],
 
663
                                          'product_qty': new_qty,
 
664
                                          'location_id': location_distrib,
 
665
                                          'location_dest_id': location_dispatch,
 
666
                                          'state': 'done'}
 
667
                                
 
668
                                # create a back move in the packing object
 
669
                                # distribution -> dispatch
 
670
                                new_back_move_id = move_obj.copy(cr, uid, move.id, values, context=dict(context, non_stock_noupdate=True))
 
671
                                updated[move.id]['partial_qty'] += new_qty
 
672
 
 
673
                                # create the draft move
 
674
                                # dispatch -> distribution
 
675
                                # picking_id = draft_picking
 
676
                                values.update(location_id=location_dispatch,
 
677
                                              location_dest_id=location_distrib,
 
678
                                              picking_id=draft_packing_id,
 
679
                                              state='assigned')
 
680
                                new_draft_move_id = move_obj.copy(cr, uid, move.id, values, context=dict(context, non_stock_noupdate=True))
 
681
                                
 
682
                            # quantities are right - stay + return qty = original qty
 
683
                            assert all([round(updated[m]['initial'], 14) == round(updated[m]['partial_qty'], 14) for m in updated.keys()]), 'initial quantity is not equal to the sum of partial quantities (%s).'%(updated)
 
684
                            # if packs are returned corresponding move is canceled
 
685
                            # cancel move or 0 qty + done ?
 
686
                            #move_obj.action_cancel(cr, uid, [move.id], context=context)
 
687
                            move_obj.write(cr, uid, [move.id], {'product_qty': 0.0, 'state': 'done', 'from_pack': 0, 'to_pack': 0,}, context=context)
 
688
            
 
689
            # log corresponding action
 
690
            shipment_name = self.read(cr, uid, shipment_id, ['name'], context=context)['name']
 
691
            self.log(cr, uid, shipment_id, _("Packs from the shipped Shipment (%s) have been returned to %s location.")%(shipment_name,dispatch_name))
 
692
            self.log(cr, uid, draft_shipment_id, _("The corresponding Draft Shipment (%s) has been updated.")%(packing.backorder_id.shipment_id.name,))
 
693
                            
 
694
        # call complete_finished on the shipment object
 
695
        # if everything is allright (all draft packing are finished) the shipment is done also 
 
696
        self.complete_finished(cr, uid, partial_datas.keys(), context=context)
 
697
        
 
698
        # TODO which behavior
 
699
        #return {'type': 'ir.actions.act_window_close'}
 
700
        data_obj = self.pool.get('ir.model.data')
 
701
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
 
702
        view_id = view_id and view_id[1] or False
 
703
        return {
 
704
            'name':_("Shipment"),
 
705
            'view_mode': 'form,tree',
 
706
            'view_id': [view_id],
 
707
            'view_type': 'form',
 
708
            'res_model': 'shipment',
 
709
            'res_id': draft_shipment_id,
 
710
            'type': 'ir.actions.act_window',
 
711
            'target': 'crush',
 
712
        }
 
713
 
 
714
        return {'type': 'ir.actions.act_window_close'}
 
715
        
 
716
    def action_cancel(self, cr, uid, ids, context=None):
 
717
        '''
 
718
        cancel the shipment which is not yet shipped (packed state)
 
719
        
 
720
        - for each related packing object
 
721
         - trigger the cancel workflow signal
 
722
         logic is performed in the action_cancel method of stock.picking
 
723
        '''
 
724
        pick_obj = self.pool.get('stock.picking')
 
725
        wf_service = netsvc.LocalService("workflow")
 
726
        
 
727
        for shipment in self.browse(cr, uid, ids, context=context):
 
728
            # shipment state should be 'packed'
 
729
            assert shipment.state == 'packed', 'cannot ship a shipment which is not in correct state - packed - %s'%shipment.state
 
730
            # for each shipment
 
731
            packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id)], context=context)
 
732
            # call cancel workflow on corresponding packing objects
 
733
            for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
 
734
                # we cancel each picking object - action_cancel is overriden at stock_picking level for stock_picking of subtype == 'packing'
 
735
                wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_cancel', cr)
 
736
            # log corresponding action
 
737
            self.log(cr, uid, shipment.id, _("The Shipment (%s) has been canceled.")%(shipment.name,))
 
738
            self.log(cr, uid, shipment.backshipment_id.id, _("The corresponding Draft Shipment (%s) has been updated.")%(shipment.backshipment_id.name,))
 
739
                
 
740
        return True
 
741
    
 
742
    def ship(self, cr, uid, ids, context=None):
 
743
        '''
 
744
        we ship the created shipment, the state of the shipment is changed, we do not use any wizard
 
745
        - state of the shipment is updated to 'shipped'
 
746
        - copy the packing
 
747
        - modify locations of moves for the new packing
 
748
        - trigger the workflow button_confirm for the new packing
 
749
        - trigger the workflow to terminate the initial packing
 
750
        - update the draft_picking_id fields of pack_families
 
751
        - update the shipment_date of the corresponding sale_order if not set yet
 
752
        '''
 
753
        pick_obj = self.pool.get('stock.picking')
 
754
        pf_obj = self.pool.get('pack.family')
 
755
        so_obj = self.pool.get('sale.order')
 
756
        # objects
 
757
        date_tools = self.pool.get('date.tools')
 
758
        db_datetime_format = date_tools.get_db_datetime_format(cr, uid, context=context)
 
759
        
 
760
        for shipment in self.browse(cr, uid, ids, context=context):
 
761
            # shipment state should be 'packed'
 
762
            assert shipment.state == 'packed', 'cannot ship a shipment which is not in correct state - packed - %s'%shipment.state
 
763
            # the state does not need to be updated - function
 
764
            # update actual ship date (shipment_actual_date) to today + time
 
765
            today = time.strftime(db_datetime_format)
 
766
            shipment.write({'shipment_actual_date': today,})
 
767
            # corresponding packing objects
 
768
            packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id)], context=context)
 
769
            
 
770
            for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
 
771
                assert packing.subtype == 'packing'
 
772
                # update the packing object for the same reason
 
773
                # - an integrity check at _get_vals level of shipment states that all packing linked to a shipment must have the same state
 
774
                # we therefore modify it before the copy, otherwise new (assigned) and old (done) are linked to the same shipment
 
775
                # -> integrity check has been removed
 
776
                pick_obj.write(cr, uid, [packing.id], {'shipment_id': False,}, context=context)
 
777
                # copy each packing
 
778
                new_packing_id = pick_obj.copy(cr, uid, packing.id, {'name': packing.name,
 
779
                                                                     'first_shipment_packing_id': packing.id,
 
780
                                                                     'shipment_id': shipment.id,}, context=dict(context, keep_prodlot=True, allow_copy=True,))
 
781
                pick_obj.write(cr, uid, [new_packing_id], {'origin': packing.origin}, context=context)
 
782
                new_packing = pick_obj.browse(cr, uid, new_packing_id, context=context)
 
783
                # update the shipment_date of the corresponding sale order if the date is not set yet - with current date
 
784
                if new_packing.sale_id and not new_packing.sale_id.shipment_date:
 
785
                    # get the date format
 
786
                    date_tools = self.pool.get('date.tools')
 
787
                    date_format = date_tools.get_date_format(cr, uid, context=context)
 
788
                    db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
 
789
                    today = time.strftime(date_format)
 
790
                    today_db = time.strftime(db_date_format)
 
791
                    so_obj.write(cr, uid, [new_packing.sale_id.id], {'shipment_date': today_db,}, context=context)
 
792
                    so_obj.log(cr, uid, new_packing.sale_id.id, _("Shipment Date of the Field Order '%s' has been updated to %s.")%(new_packing.sale_id.name, today))
 
793
                
 
794
                # update locations of stock moves
 
795
                for move in new_packing.move_lines:
 
796
                    move.write({'location_id': new_packing.warehouse_id.lot_distribution_id.id,
 
797
                                'location_dest_id': new_packing.warehouse_id.lot_output_id.id}, context=context)
 
798
                
 
799
                wf_service = netsvc.LocalService("workflow")
 
800
                wf_service.trg_validate(uid, 'stock.picking', new_packing_id, 'button_confirm', cr)
 
801
                # simulate check assign button, as stock move must be available
 
802
                pick_obj.force_assign(cr, uid, [new_packing_id])
 
803
                # trigger standard workflow
 
804
                pick_obj.action_move(cr, uid, [packing.id])
 
805
                wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_done', cr)
 
806
                
 
807
            # log the ship action
 
808
            self.log(cr, uid, shipment.id, _('The Shipment %s has been shipped.')%(shipment.name,))
 
809
    
 
810
        # TODO which behavior
 
811
        return True
 
812
    
 
813
    def complete_finished(self, cr, uid, ids, context=None):
 
814
        '''
 
815
        - check all draft packing corresponding to this shipment
 
816
          - check the stock moves (qty and from/to)
 
817
          - check all corresponding packing are done or canceled (no ongoing shipment)
 
818
          - if all packings are ok, the draft is validated
 
819
        - if all draft packing are ok, the shipment state is done
 
820
        '''
 
821
        pick_obj = self.pool.get('stock.picking')
 
822
        wf_service = netsvc.LocalService("workflow")
 
823
        
 
824
        for shipment_base in self.browse(cr, uid, ids, context=context):
 
825
            # the shipment which will be treated
 
826
            shipment = shipment_base
 
827
            
 
828
            if shipment.state not in ('draft',):
 
829
                # it's not a draft shipment, check all corresponding packing, trg.write them
 
830
                packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
 
831
                for packing_id in packing_ids:
 
832
                    wf_service.trg_write(uid, 'stock.picking', packing_id, cr)
 
833
                
 
834
                # this shipment is possibly finished, we now check the corresponding draft shipment
 
835
                # this will possibly validate the draft shipment, if everything is finished and corresponding draft picking
 
836
                shipment = shipment.backshipment_id
 
837
                
 
838
            # draft packing for this shipment - some draft packing can already be done for this shipment, so we filter according to state
 
839
            draft_packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '=', 'draft'),], context=context)
 
840
            for draft_packing in pick_obj.browse(cr, uid, draft_packing_ids, context=context):
 
841
                assert draft_packing.subtype == 'packing', 'draft packing which is not packing subtype - %s'%draft_packing.subtype
 
842
                assert draft_packing.state == 'draft', 'draft packing which is not draft state - %s'%draft_packing.state
 
843
                # we check if the corresponding draft packing can be moved to done.
 
844
                # if all packing with backorder_id equal to draft are done or canceled
 
845
                # and the quantity for each stock move (state != done) of the draft packing is equal to zero
 
846
                
 
847
                # we first check the stock moves quantities of the draft packing
 
848
                # we can have done moves when some packs are returned
 
849
                treat_draft = True
 
850
                for move in draft_packing.move_lines:
 
851
                    if move.state not in ('done',):
 
852
                        if move.product_qty:
 
853
                            treat_draft = False
 
854
                        elif move.from_pack or move.to_pack:
 
855
                            # qty = 0, from/to pack should have been set to zero
 
856
                            assert False, 'stock moves with 0 quantity but part of pack family sequence'
 
857
                
 
858
                # check if ongoing packing are present, if present, we do not validate the draft one, the shipping is not finished
 
859
                if treat_draft:
 
860
                    linked_packing_ids = pick_obj.search(cr, uid, [('backorder_id', '=', draft_packing.id),
 
861
                                                                   ('state', 'not in', ['done', 'cancel'])], context=context)
 
862
                    if linked_packing_ids:
 
863
                        treat_draft = False
 
864
                
 
865
                if treat_draft:
 
866
                    # trigger the workflow for draft_picking
 
867
                    # confirm the new picking ticket
 
868
                    wf_service.trg_validate(uid, 'stock.picking', draft_packing.id, 'button_confirm', cr)
 
869
                    # we force availability
 
870
                    pick_obj.force_assign(cr, uid, [draft_packing.id])
 
871
                    # finish
 
872
                    pick_obj.action_move(cr, uid, [draft_packing.id])
 
873
                    wf_service.trg_validate(uid, 'stock.picking', draft_packing.id, 'button_done', cr)
 
874
                    # ask for draft picking validation, depending on picking completion
 
875
                    # if picking ticket is not completed, the validation will not complete
 
876
                    draft_packing.previous_step_id.previous_step_id.backorder_id.validate(context=context)
 
877
            
 
878
            # all draft packing are validated (done state) - the state of shipment is automatically updated -> function
 
879
        return True
 
880
    
 
881
    def shipment_create_invoice(self, cr, uid, ids, context=None):
 
882
        '''
 
883
        Create invoices for validated shipment
 
884
        '''
 
885
        invoice_obj = self.pool.get('account.invoice')
 
886
        line_obj = self.pool.get('account.invoice.line')
 
887
        partner_obj = self.pool.get('res.partner')
 
888
        distrib_obj = self.pool.get('analytic.distribution')
 
889
        sale_line_obj = self.pool.get('sale.order.line')
 
890
        sale_obj = self.pool.get('sale.order')
 
891
        company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
 
892
        
 
893
        if not context:
 
894
            context = {}
 
895
            
 
896
        if isinstance(ids, (int, long)):
 
897
            ids = [ids]
 
898
            
 
899
        for shipment in self.browse(cr, uid, ids, context=context):
 
900
            make_invoice = False
 
901
            for pack in shipment.pack_family_memory_ids:
 
902
                for move in pack.move_lines:
 
903
                    if move.state != 'cancel' and (not move.sale_line_id or move.sale_line_id.order_id.order_policy == 'picking'):
 
904
                        make_invoice = True
 
905
                    
 
906
            if not make_invoice:
 
907
                continue
 
908
                    
 
909
            payment_term_id = False
 
910
            partner =  shipment.partner_id2
 
911
            if not partner:
 
912
                raise osv.except_osv(_('Error, no partner !'),
 
913
                    _('Please put a partner on the shipment if you want to generate invoice.'))
 
914
            
 
915
            inv_type = 'out_invoice'
 
916
            
 
917
            if inv_type in ('out_invoice', 'out_refund'):
 
918
                account_id = partner.property_account_receivable.id
 
919
                payment_term_id = partner.property_payment_term and partner.property_payment_term.id or False
 
920
            else:
 
921
                account_id = partner.property_account_payable.id
 
922
            
 
923
            addresses = partner_obj.address_get(cr, uid, [partner.id], ['contact', 'invoice'])
 
924
            today = time.strftime('%Y-%m-%d',time.localtime())  
 
925
            
 
926
            invoice_vals = {
 
927
                    'name': shipment.name,
 
928
                    'origin': shipment.name or '',
 
929
                    'type': inv_type,
 
930
                    'account_id': account_id,
 
931
                    'partner_id': partner.id,
 
932
                    'address_invoice_id': addresses['invoice'],
 
933
                    'address_contact_id': addresses['contact'],
 
934
                    'payment_term': payment_term_id,
 
935
                    'fiscal_position': partner.property_account_position.id,
 
936
                    'date_invoice': context.get('date_inv',False) or today,
 
937
                    'user_id':uid,
 
938
                }
 
939
 
 
940
            cur_id = shipment.pack_family_memory_ids[0].currency_id.id
 
941
            if cur_id:
 
942
                invoice_vals['currency_id'] = cur_id
 
943
            # Journal type
 
944
            journal_type = 'sale'
 
945
            # Disturb journal for invoice only on intermission partner type
 
946
            if shipment.partner_id2.partner_type == 'intermission':
 
947
                if not company.intermission_default_counterpart or not company.intermission_default_counterpart.id:
 
948
                    raise osv.except_osv(_('Error'), _('Please configure a default intermission account in Company configuration.'))
 
949
                invoice_vals['is_intermission'] = True
 
950
                invoice_vals['account_id'] = company.intermission_default_counterpart.id
 
951
                journal_type = 'intermission'
 
952
            journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', journal_type),
 
953
                                                                            ('is_current_instance', '=', True)])
 
954
            if not journal_ids:
 
955
                raise osv.except_osv(_('Warning'), _('No %s journal found!') % (journal_type,))
 
956
            invoice_vals['journal_id'] = journal_ids[0]
 
957
                
 
958
            invoice_id = invoice_obj.create(cr, uid, invoice_vals,
 
959
                        context=context)
 
960
            
 
961
            # Change currency for the intermission invoice
 
962
            if shipment.partner_id2.partner_type == 'intermission':
 
963
                company_currency = company.currency_id and company.currency_id.id or False
 
964
                if not company_currency:
 
965
                    raise osv.except_osv(_('Warning'), _('No company currency found!'))
 
966
                wiz_account_change = self.pool.get('account.change.currency').create(cr, uid, {'currency_id': company_currency}, context=context)
 
967
                self.pool.get('account.change.currency').change_currency(cr, uid, [wiz_account_change], context={'active_id': invoice_id})
 
968
            
 
969
            # Link the invoice to the shipment
 
970
            self.write(cr, uid, [shipment.id], {'invoice_id': invoice_id}, context=context)
 
971
            
 
972
            # For each stock moves, create an invoice line
 
973
            for pack in shipment.pack_family_memory_ids:
 
974
                for move in pack.move_lines:
 
975
                    if move.state == 'cancel':
 
976
                        continue
 
977
                    
 
978
                    if move.sale_line_id and move.sale_line_id.order_id.order_policy != 'picking':
 
979
                        continue
 
980
                    
 
981
                    origin = move.picking_id.name or ''
 
982
                    if move.picking_id.origin:
 
983
                        origin += ':' + move.picking_id.origin
 
984
                        
 
985
                    if inv_type in ('out_invoice', 'out_refund'):
 
986
                        account_id = move.product_id.product_tmpl_id.\
 
987
                                property_account_income.id
 
988
                        if not account_id:
 
989
                            account_id = move.product_id.categ_id.\
 
990
                                    property_account_income_categ.id
 
991
                    else:
 
992
                        account_id = move.product_id.product_tmpl_id.\
 
993
                                property_account_expense.id
 
994
                        if not account_id:
 
995
                            account_id = move.product_id.categ_id.\
 
996
                                    property_account_expense_categ.id
 
997
                                    
 
998
                    # Compute unit price from FO line if the move is linked to
 
999
                    price_unit = move.product_id.list_price
 
1000
                    if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
 
1001
                        uom_id = move.product_id.uom_id.id
 
1002
                        uos_id = move.product_id.uos_id and move.product_id.uos_id.id or False
 
1003
                        price = move.sale_line_id.price_unit
 
1004
                        coeff = move.product_id.uos_coeff
 
1005
                        if uom_id != uos_id and coeff != 0:
 
1006
                            price_unit = price / coeff
 
1007
                        else:
 
1008
                            price_unit = move.sale_line_id.price_unit
 
1009
                            
 
1010
                    # Get discount from FO line
 
1011
                    discount = 0.00
 
1012
                    if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
 
1013
                        discount = move.sale_line_id.discount
 
1014
                        
 
1015
                    # Get taxes from FO line
 
1016
                    taxes = move.product_id.taxes_id
 
1017
                    if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
 
1018
                        taxes = [x.id for x in move.sale_line_id.tax_id]
 
1019
                        
 
1020
                    if shipment.partner_id2:
 
1021
                        tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, shipment.partner_id2.property_account_position, taxes)
 
1022
                    else:
 
1023
                        tax_ids = map(lambda x: x.id, taxes)
 
1024
                        
 
1025
                    distrib_id = False
 
1026
                    if move.sale_line_id:
 
1027
                        sol_ana_dist_id = move.sale_line_id.analytic_distribution_id or move.sale_line_id.order_id.analytic_distribution_id
 
1028
                        if sol_ana_dist_id:
 
1029
                            distrib_id = distrib_obj.copy(cr, uid, sol_ana_dist_id.id, context=context)
 
1030
                        
 
1031
                    #set UoS if it's a sale and the picking doesn't have one
 
1032
                    uos_id = move.product_uos and move.product_uos.id or False
 
1033
                    if not uos_id and inv_type in ('out_invoice', 'out_refund'):
 
1034
                        uos_id = move.product_uom.id
 
1035
                    account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
 
1036
                        
 
1037
                    line_id = line_obj.create(cr, uid, {'name': move.name,
 
1038
                                                        'origin': origin,
 
1039
                                                        'invoice_id': invoice_id,
 
1040
                                                        'uos_id': uos_id,
 
1041
                                                        'product_id': move.product_id.id,
 
1042
                                                        'account_id': account_id,
 
1043
                                                        'price_unit': price_unit,
 
1044
                                                        'discount': discount,
 
1045
                                                        'quantity': move.product_qty or move.product_uos_qty,
 
1046
                                                        'invoice_line_tax_id': [(6, 0, tax_ids)],
 
1047
                                                        'analytic_distribution_id': distrib_id,
 
1048
                                                       }, context=context)
 
1049
 
 
1050
                    self.pool.get('shipment').write(cr, uid, [shipment.id], {'invoice_id': invoice_id}, context=context)
 
1051
                    if move.sale_line_id:
 
1052
                        sale_obj.write(cr, uid, [move.sale_line_id.order_id.id], {'invoice_ids': [(4, invoice_id)],})
 
1053
                        sale_line_obj.write(cr, uid, [move.sale_line_id.id], {'invoiced': True,
 
1054
                                                                              'invoice_lines': [(4, line_id)],})
 
1055
            
 
1056
        return True
 
1057
        
 
1058
    def validate(self, cr, uid, ids, context=None):
 
1059
        '''
 
1060
        validate the shipment
 
1061
        
 
1062
        change the state to Done for the corresponding packing
 
1063
        - validate the workflow for all the packings
 
1064
        '''
 
1065
        pick_obj = self.pool.get('stock.picking')
 
1066
        wf_service = netsvc.LocalService("workflow")
 
1067
        
 
1068
        for shipment in self.browse(cr, uid, ids, context=context):
 
1069
            # validate should only be called on shipped shipments
 
1070
            assert shipment.state in ('shipped',), 'shipment state is not shipped'
 
1071
            # corresponding packing objects - only the distribution -> customer ones
 
1072
            # we have to discard picking object with state done, because when we return from shipment
 
1073
            # all object of a given picking object, he is set to Done and still belong to the same shipment_id
 
1074
            # another possibility would be to unlink the picking object from the shipment, set shipment_id to False
 
1075
            # but in this case the returned pack families would not be displayed anymore in the shipment
 
1076
            packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '!=', 'done'),], context=context)
 
1077
            
 
1078
            for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
 
1079
                assert packing.subtype == 'packing' and packing.state == 'assigned'
 
1080
                # trigger standard workflow
 
1081
                pick_obj.action_move(cr, uid, [packing.id])
 
1082
                wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_done', cr)
 
1083
            
 
1084
            # Create automatically the invoice
 
1085
            self.shipment_create_invoice(cr, uid, shipment.id, context=context)
 
1086
                
 
1087
            # log validate action
 
1088
            self.log(cr, uid, shipment.id, _('The Shipment %s has been closed.')%(shipment.name,))
 
1089
            
 
1090
        result = self.complete_finished(cr, uid, ids, context=context)
 
1091
        return True
 
1092
    
 
1093
    def set_delivered(self, cr, uid, ids, context=None):
 
1094
        '''
 
1095
        set the delivered flag
 
1096
        '''
 
1097
        # objects
 
1098
        pick_obj = self.pool.get('stock.picking')
 
1099
        for shipment in self.browse(cr, uid, ids, context=context):
 
1100
            # validate should only be called on shipped shipments
 
1101
            assert shipment.state in ['done'], 'shipment state is not shipped'
 
1102
            # gather the corresponding packing and trigger the corresponding function
 
1103
            packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '=', 'done')], context=context)
 
1104
            # set delivered all packings
 
1105
            pick_obj.set_delivered(cr, uid, packing_ids, context=context)
 
1106
            
 
1107
        return True
 
1108
        
 
1109
shipment()
 
1110
 
 
1111
 
 
1112
class pack_family_memory(osv.osv_memory):
 
1113
    '''
 
1114
    dynamic memory object for pack families
 
1115
    '''
 
1116
    _name = 'pack.family.memory'
 
1117
    
 
1118
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
1119
        '''
 
1120
        get functional values
 
1121
        '''
 
1122
        result = {}
 
1123
        for pf_memory in self.browse(cr, uid, ids, context=context):
 
1124
            values = {'move_lines': [],
 
1125
                      'state': 'draft',
 
1126
                      'location_id': False,
 
1127
                      'location_dest_id': False,
 
1128
                      'total_amount': 0.0,
 
1129
                      'amount': 0.0,
 
1130
                      'currency_id': False,
 
1131
                      'num_of_packs': 0,
 
1132
                      'total_weight': 0.0,
 
1133
                      'total_volume': 0.0,
 
1134
                      }
 
1135
            result[pf_memory.id] = values
 
1136
            # pack family related fields
 
1137
            if pf_memory.to_pack == 0:
 
1138
                num_of_packs = 0
 
1139
            else:
 
1140
                num_of_packs = pf_memory.to_pack - pf_memory.from_pack + 1
 
1141
            values['num_of_packs'] = num_of_packs
 
1142
            values['total_weight'] = pf_memory.weight * num_of_packs
 
1143
            values['total_volume'] = (pf_memory.length * pf_memory.width * pf_memory.height * num_of_packs) / 1000.0
 
1144
            
 
1145
            # moves related fields
 
1146
            for move in pf_memory.draft_packing_id.move_lines:
 
1147
                if move.from_pack == pf_memory.from_pack:
 
1148
                    if move.to_pack == pf_memory.to_pack:
 
1149
                        # this move is in the good packing object and corresponds to this pack family
 
1150
                        # we add it to the stock move list
 
1151
                        values['move_lines'].append(move.id)
 
1152
                        values['state'] = move.state
 
1153
                        values['location_id'] = move.location_id.id
 
1154
                        values['location_dest_id'] = move.location_dest_id.id
 
1155
                        values['total_amount'] += move.total_amount
 
1156
                        values['amount'] += move.amount
 
1157
                        values['currency_id'] = move.currency_id and move.currency_id.id or False
 
1158
                    else:
 
1159
                        # when multiple moves are modified from/to values, the first one would raise an exception as the second one is not written yet
 
1160
                        pass
 
1161
                        #raise osv.except_osv(_('Error !'), _('Integrity check failed! Pack Family and Stock Moves from/to do not match.'))
 
1162
                    
 
1163
        return result
 
1164
 
 
1165
    _columns = {'name': fields.char(string='Reference', size=1024),
 
1166
                'shipment_id': fields.many2one('shipment', string='Shipment'),
 
1167
                'draft_packing_id': fields.many2one('stock.picking', string="Draft Packing Ref"),
 
1168
                'sale_order_id': fields.many2one('sale.order', string="Sale Order Ref"),
 
1169
                'ppl_id': fields.many2one('stock.picking', string="PPL Ref"),
 
1170
                'from_pack': fields.integer(string='From p.'),
 
1171
                'to_pack': fields.integer(string='To p.'),
 
1172
                'pack_type': fields.many2one('pack.type', string='Pack Type'),
 
1173
                'length' : fields.float(digits=(16,2), string='Length [cm]'),
 
1174
                'width' : fields.float(digits=(16,2), string='Width [cm]'),
 
1175
                'height' : fields.float(digits=(16,2), string='Height [cm]'),
 
1176
                'weight' : fields.float(digits=(16,2), string='Weight p.p [kg]'),
 
1177
                # functions
 
1178
                'move_lines': fields.function(_vals_get, method=True, type='one2many', relation='stock.move', string='Stock Moves', multi='get_vals',),
 
1179
                'state': fields.function(_vals_get, method=True, type='selection', selection=[('draft', 'Draft'),
 
1180
                                                                                              ('assigned', 'Available'),
 
1181
                                                                                              ('stock_return', 'Returned to Stock'),
 
1182
                                                                                              ('ship_return', 'Returned from Shipment'),
 
1183
                                                                                              ('cancel', 'Cancelled'),
 
1184
                                                                                              ('done', 'Closed'),], string='State', multi='get_vals',),
 
1185
                'location_id': fields.function(_vals_get, method=True, type='many2one', relation='stock.location', string='Src Loc.', multi='get_vals',),
 
1186
                'location_dest_id': fields.function(_vals_get, method=True, type='many2one', relation='stock.location', string='Dest. Loc.', multi='get_vals',),
 
1187
                'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', multi='get_vals',),
 
1188
                'amount': fields.function(_vals_get, method=True, type='float', string='Pack Amount', multi='get_vals',),
 
1189
                'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
 
1190
                'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals',),
 
1191
                'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals',),
 
1192
                'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals',),
 
1193
                'description_ppl': fields.char('Description', size=256 ),
 
1194
                }
 
1195
    
 
1196
    _defaults = {'shipment_id': False,
 
1197
                 'draft_packing_id': False,
 
1198
                 }
 
1199
    
 
1200
pack_family_memory()
 
1201
 
 
1202
 
 
1203
class shipment2(osv.osv):
 
1204
    '''
 
1205
    add pack_family_ids
 
1206
    '''
 
1207
    _inherit = 'shipment'
 
1208
    
 
1209
    def on_change_partner(self, cr, uid, ids, partner_id, address_id, context=None):
 
1210
        '''
 
1211
        Change the delivery address when the partner change.
 
1212
        '''
 
1213
        v = {}
 
1214
        d = {}
 
1215
        
 
1216
        if not partner_id:
 
1217
            v.update({'address_id': False})
 
1218
        else:
 
1219
            d.update({'address_id': [('partner_id', '=', partner_id)]})
 
1220
            
 
1221
 
 
1222
        if address_id:
 
1223
            addr = self.pool.get('res.partner.address').browse(cr, uid, address_id, context=context)
 
1224
        
 
1225
        if not address_id or addr.partner_id.id != partner_id:
 
1226
            addr = self.pool.get('res.partner').address_get(cr, uid, partner_id, ['delivery', 'default'])
 
1227
            if not addr.get('delivery'):
 
1228
                addr = addr.get('default')
 
1229
            else:
 
1230
                addr = addr.get('delivery')
 
1231
                
 
1232
            v.update({'address_id': addr})
 
1233
            
 
1234
        
 
1235
        return {'value': v,
 
1236
                'domain': d}
 
1237
    
 
1238
    def _vals_get_2(self, cr, uid, ids, fields, arg, context=None):
 
1239
        '''
 
1240
        get functional values
 
1241
        '''
 
1242
        picking_obj = self.pool.get('stock.picking')
 
1243
        
 
1244
        result = {}
 
1245
        for shipment in self.browse(cr, uid, ids, context=context):
 
1246
            values = {'pack_family_memory_ids':[],
 
1247
                      }
 
1248
            result[shipment.id] = values
 
1249
            # look for all corresponding packing
 
1250
            packing_ids = picking_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
 
1251
            # get the corresponding data
 
1252
            data = picking_obj.generate_data_from_picking_for_pack_family(cr, uid, packing_ids, context=context)
 
1253
            # create a memory family
 
1254
            created_ids = picking_obj.create_pack_families_memory_from_data(cr, uid, data, shipment.id, context=context)
 
1255
            values['pack_family_memory_ids'].extend(created_ids)
 
1256
            
 
1257
        return result
 
1258
    
 
1259
    _columns = {'pack_family_memory_ids': fields.function(_vals_get_2, method=True, type='one2many', relation='pack.family.memory', string='Memory Families', multi='get_vals_2',),
 
1260
                }
 
1261
 
 
1262
shipment2()
 
1263
 
 
1264
 
 
1265
class ppl_customize_label(osv.osv):
 
1266
    '''
 
1267
    label preferences
 
1268
    '''
 
1269
    _name = 'ppl.customize.label'
 
1270
    
 
1271
    def init(self, cr):
 
1272
        """
 
1273
        Load msf_outgoing_data.xml before self
 
1274
        """
 
1275
        if hasattr(super(ppl_customize_label, self), 'init'):
 
1276
            super(ppl_customize_label, self).init(cr)
 
1277
 
 
1278
        mod_obj = self.pool.get('ir.module.module')
 
1279
        logging.getLogger('init').info('HOOK: module msf_outgoing: loading data/msf_outgoing_data.xml')
 
1280
        pathname = path.join('msf_outgoing', 'data/msf_outgoing_data.xml')
 
1281
        file = tools.file_open(pathname)
 
1282
        tools.convert_xml_import(cr, 'msf_outgoing', file, {}, mode='init', noupdate=False)
 
1283
 
 
1284
    _columns = {'name': fields.char(string='Name', size=1024,),
 
1285
                'notes': fields.text(string='Notes'),
 
1286
                #'packing_list_reference': fields.boolean(string='Packing List Reference'),
 
1287
                'pre_packing_list_reference': fields.boolean(string='Pre-Packing List Reference'),
 
1288
                'destination_partner': fields.boolean(string='Destination Partner'),
 
1289
                'destination_address': fields.boolean(string='Destination Address'),
 
1290
                'requestor_order_reference': fields.boolean(string='Requestor Order Reference'),
 
1291
                'weight': fields.boolean(string='Weight'),
 
1292
                #'shipment_reference': fields.boolean(string='Shipment Reference'),
 
1293
                'packing_parcel_number': fields.boolean(string='Packing Parcel Number'),
 
1294
                #'expedition_parcel_number': fields.boolean(string='Expedition Parcel Number'),
 
1295
                'specific_information': fields.boolean(string='Specific Information'),
 
1296
                'logo': fields.boolean(string='Company Logo'),
 
1297
                }
 
1298
    
 
1299
    _defaults = {'name': 'My Customization',
 
1300
                'notes': '',
 
1301
                #'packing_list_reference': True,
 
1302
                'pre_packing_list_reference': True,
 
1303
                'destination_partner': True,
 
1304
                'destination_address': True,
 
1305
                'requestor_order_reference': True,
 
1306
                'weight': True,
 
1307
                #'shipment_reference': True,
 
1308
                'packing_parcel_number': True,
 
1309
                #'expedition_parcel_number': True,
 
1310
                'specific_information': True,
 
1311
                'logo': True,
 
1312
                }
 
1313
 
 
1314
ppl_customize_label()
 
1315
 
 
1316
 
 
1317
class stock_picking(osv.osv):
 
1318
    '''
 
1319
    override stock picking to add new attributes
 
1320
    - flow_type: the type of flow (full, quick)
 
1321
    - subtype: the subtype of picking object (picking, ppl, packing)
 
1322
    - previous_step_id: the id of picking object of the previous step, picking for ppl, ppl for packing
 
1323
    '''
 
1324
    _inherit = 'stock.picking'
 
1325
    _name = 'stock.picking'
 
1326
 
 
1327
    def fields_view_get(self, cr, uid, view_id, view_type, context=None, toolbar=False, submenu=False):
 
1328
        '''
 
1329
        Set the appropriate search view according to the context
 
1330
        '''
 
1331
        if not context:
 
1332
            context = {}
 
1333
 
 
1334
        if not view_id and context.get('wh_dashboard') and view_type == 'search':
 
1335
            try:
 
1336
                if context.get('pick_type') == 'incoming':
 
1337
                    view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'view_picking_in_search')[1]
 
1338
                elif context.get('pick_type') == 'delivery':
 
1339
                    view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'view_picking_out_search')[1]
 
1340
                elif context.get('pick_type') == 'picking_ticket':
 
1341
                    view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_search')[1]
 
1342
                elif context.get('pick_type') == 'pack':
 
1343
                    view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_search')[1]
 
1344
            except ValueError:
 
1345
                pass
 
1346
 
 
1347
        return super(stock_picking, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
 
1348
    
 
1349
    def unlink(self, cr, uid, ids, context=None):
 
1350
        '''
 
1351
        unlink test for draft
 
1352
        '''
 
1353
        datas = self.read(cr, uid, ids, ['state','type','subtype'], context=context)
 
1354
        if [data for data in datas if data['state'] != 'draft']:
 
1355
            raise osv.except_osv(_('Warning !'), _('Only draft picking tickets can be deleted.'))
 
1356
        ids_picking_draft = [data['id'] for data in datas if data['subtype'] == 'picking' and data['type'] == 'out' and data['state'] == 'draft']
 
1357
        if ids_picking_draft:
 
1358
            data = self.has_picking_ticket_in_progress(cr, uid, ids, context=context)
 
1359
            if [x for x in data.values() if x]:
 
1360
                raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try again.'))
 
1361
        
 
1362
        return super(stock_picking, self).unlink(cr, uid, ids, context=context)
 
1363
   
 
1364
    def _hook_picking_get_view(self, cr, uid, ids, context=None, *args, **kwargs):
 
1365
        pick = kwargs['pick']
 
1366
        obj_data = self.pool.get('ir.model.data')
 
1367
        view_list = {'standard': ('stock', 'view_picking_out_form'),
 
1368
                     'picking': ('msf_outgoing', 'view_picking_ticket_form'),
 
1369
                     'ppl': ('msf_outgoing', 'view_ppl_form'),
 
1370
                     'packing': ('msf_outgoing', 'view_packing_form'),
 
1371
                     }
 
1372
        if pick.type == 'out':
 
1373
            context.update({'picking_type': pick.subtype == 'standard' and 'delivery_order' or 'picking_ticket'})
 
1374
            module, view = view_list.get(pick.subtype,('msf_outgoing', 'view_picking_ticket_form'))
 
1375
            try:
 
1376
                return obj_data.get_object_reference(cr, uid, module, view)
 
1377
            except ValueError, e:
 
1378
                pass
 
1379
        elif pick.type == 'in':
 
1380
            context.update({'picking_type': 'incoming_shipment'})
 
1381
        else:
 
1382
            context.update({'picking_type': 'internal_move'})
 
1383
        
 
1384
        return super(stock_picking, self)._hook_picking_get_view(cr, uid, ids, context=context, *args, **kwargs)
 
1385
 
 
1386
    def _hook_custom_log(self, cr, uid, ids, context=None, *args, **kwargs):
 
1387
        '''
 
1388
        hook from stock>stock.py>log_picking
 
1389
        update the domain and other values if necessary in the log creation
 
1390
        '''
 
1391
        result = super(stock_picking, self)._hook_custom_log(cr, uid, ids, context=context, *args, **kwargs)
 
1392
        pick_obj = self.pool.get('stock.picking')
 
1393
        pick = kwargs['pick']
 
1394
        message = kwargs['message']
 
1395
        if pick.type and pick.subtype:
 
1396
            domain = [('type', '=', pick.type), ('subtype', '=', pick.subtype)]
 
1397
            return self.pool.get('res.log').create(cr, uid,
 
1398
                                                   {'name': message,
 
1399
                                                    'res_model': pick_obj._name,
 
1400
                                                    'secondary': False,
 
1401
                                                    'res_id': pick.id,
 
1402
                                                    'domain': domain,
 
1403
                                                    }, context=context)
 
1404
        return result
 
1405
 
 
1406
    def _hook_log_picking_log_cond(self, cr, uid, ids, context=None, *args, **kwargs):
 
1407
        '''
 
1408
        hook from stock>stock.py>stock_picking>log_picking
 
1409
        specify if we display a log or not
 
1410
        '''
 
1411
        result = super(stock_picking, self)._hook_log_picking_log_cond(cr, uid, ids, context=context, *args, **kwargs)
 
1412
        pick = kwargs['pick']
 
1413
        if pick.subtype == 'packing':
 
1414
            return 'packing'
 
1415
        # if false the log will be defined by the method _hook_custom_log (which include a domain)
 
1416
        if pick.type and pick.subtype:
 
1417
            return False
 
1418
 
 
1419
        return result
 
1420
    
 
1421
    def copy(self, cr, uid, id, default=None, context=None):
 
1422
        '''
 
1423
        set the name corresponding to object subtype
 
1424
        '''
 
1425
        if default is None:
 
1426
            default = {}
 
1427
        if context is None:
 
1428
            context = {}
 
1429
        obj = self.browse(cr, uid, id, context=context)
 
1430
        if not context.get('allow_copy', False):
 
1431
            if obj.subtype == 'picking':
 
1432
                if not obj.backorder_id:
 
1433
                    # draft, new ref
 
1434
                    default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket'),
 
1435
                                   origin=False,
 
1436
                                   date=date.today().strftime('%Y-%m-%d'),
 
1437
                                   sale_id=False,
 
1438
                                   )
 
1439
                else:
 
1440
                    # if the corresponding draft picking ticket is done, we do not allow copy
 
1441
                    if obj.backorder_id and obj.backorder_id.state == 'done':
 
1442
                        raise osv.except_osv(_('Error !'), _('Corresponding Draft picking ticket is Closed. This picking ticket cannot be copied.'))
 
1443
                    # picking ticket, use draft sequence, keep other fields
 
1444
                    base = obj.name
 
1445
                    base = base.split('-')[0] + '-'
 
1446
                    default.update(name=base + obj.backorder_id.sequence_id.get_id(test='id', context=context),
 
1447
                                   date=date.today().strftime('%Y-%m-%d'),
 
1448
                                   )
 
1449
                    
 
1450
            elif obj.subtype == 'ppl':
 
1451
                raise osv.except_osv(_('Error !'), _('Pre-Packing List copy is forbidden.'))
 
1452
                # ppl, use the draft picking ticket sequence
 
1453
#                if obj.previous_step_id and obj.previous_step_id.backorder_id:
 
1454
#                    base = obj.name
 
1455
#                    base = base.split('-')[0] + '-'
 
1456
#                    default.update(name=base + obj.previous_step_id.backorder_id.sequence_id.get_id(test='id', context=context))
 
1457
#                else:
 
1458
#                    default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'ppl'))
 
1459
                
 
1460
        result = super(stock_picking, self).copy(cr, uid, id, default=default, context=context)
 
1461
        if not context.get('allow_copy', False):
 
1462
            if obj.subtype == 'picking' and obj.backorder_id:
 
1463
                # confirm the new picking ticket - the picking ticket should not stay in draft state !
 
1464
                wf_service = netsvc.LocalService("workflow")
 
1465
                wf_service.trg_validate(uid, 'stock.picking', result, 'button_confirm', cr)
 
1466
                # we force availability
 
1467
                self.force_assign(cr, uid, [result])
 
1468
        return result
 
1469
    
 
1470
    def copy_data(self, cr, uid, id, default=None, context=None):
 
1471
        '''
 
1472
        reset one2many fields
 
1473
        '''
 
1474
        if default is None:
 
1475
            default = {}
 
1476
        if context is None:
 
1477
            context = {}
 
1478
        # reset one2many fields
 
1479
        default.update(backorder_ids=[])
 
1480
        default.update(previous_step_ids=[])
 
1481
        default.update(pack_family_memory_ids=[])
 
1482
        # the tag 'from_button' was added in the web client (openerp/controllers/form.py in the method duplicate) on purpose
 
1483
        if context.get('from_button'):
 
1484
            default.update(purchase_id=False)
 
1485
        context['not_workflow'] = True
 
1486
        result = super(stock_picking, self).copy_data(cr, uid, id, default=default, context=context)
 
1487
        
 
1488
        return result
 
1489
    
 
1490
    def _erase_prodlot_hook(self, cr, uid, id, context=None, *args, **kwargs):
 
1491
        '''
 
1492
        hook to keep the production lot when a stock move is copied
 
1493
        '''
 
1494
        res = super(stock_picking, self)._erase_prodlot_hook(cr, uid, id, context=context, *args, **kwargs)
 
1495
        
 
1496
        return res and not context.get('keep_prodlot', False)
 
1497
    
 
1498
    def has_picking_ticket_in_progress(self, cr, uid, ids, context=None):
 
1499
        '''
 
1500
        ids is the list of draft picking object we want to test
 
1501
        completed means, we recursively check that next_step link object is cancel or done
 
1502
        
 
1503
        return true if picking tickets are in progress, meaning picking ticket or ppl or shipment not done exist
 
1504
        '''
 
1505
        if context is None:
 
1506
            context = {}
 
1507
        if isinstance(ids, (int, long)):
 
1508
            ids = []
 
1509
        res = {}
 
1510
        for obj in self.browse(cr, uid, ids, context=context):
 
1511
            # by default, nothing is in progress
 
1512
            res[obj.id] = False
 
1513
            # treat only draft picking
 
1514
            assert obj.subtype in 'picking' and obj.state == 'draft', 'the validate function should only be called on draft picking ticket objects'
 
1515
            for picking in obj.backorder_ids:
 
1516
                # take care, is_completed returns a dictionary
 
1517
                if not picking.is_completed()[picking.id]:
 
1518
                    res[obj.id] = True
 
1519
                    break
 
1520
        
 
1521
        return res
 
1522
    
 
1523
    def validate(self, cr, uid, ids, context=None):
 
1524
        '''
 
1525
        validate or not the draft picking ticket
 
1526
        '''
 
1527
        # objects
 
1528
        move_obj = self.pool.get('stock.move')
 
1529
        
 
1530
        for draft_picking in self.browse(cr, uid, ids, context=context):
 
1531
            # the validate function should only be called on draft picking ticket
 
1532
            assert draft_picking.subtype == 'picking' and draft_picking.state == 'draft', 'the validate function should only be called on draft picking ticket objects'
 
1533
            #check the qty of all stock moves
 
1534
            treat_draft = True
 
1535
            move_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_picking.id),
 
1536
                                                 ('product_qty', '!=', 0.0),
 
1537
                                                 ('state', 'not in', ['done', 'cancel'])], context=context)
 
1538
            if move_ids:
 
1539
                treat_draft = False
 
1540
            
 
1541
            if treat_draft:
 
1542
                # then all child picking must be fully completed, meaning:
 
1543
                # - all picking must be 'completed'
 
1544
                # completed means, we recursively check that next_step link object is cancel or done
 
1545
                if self.has_picking_ticket_in_progress(cr, uid, [draft_picking.id], context=context)[draft_picking.id]:
 
1546
                    treat_draft = False
 
1547
            
 
1548
            if treat_draft:
 
1549
                # - all picking are completed (means ppl completed and all shipment validated)
 
1550
                wf_service = netsvc.LocalService("workflow")
 
1551
                wf_service.trg_validate(uid, 'stock.picking', draft_picking.id, 'button_confirm', cr)
 
1552
                # we force availability
 
1553
                draft_picking.force_assign()
 
1554
                # finish
 
1555
                draft_picking.action_move()
 
1556
                wf_service.trg_validate(uid, 'stock.picking', draft_picking.id, 'button_done', cr)
 
1557
                
 
1558
        return True
 
1559
    
 
1560
    def _vals_get_2(self, cr, uid, ids, fields, arg, context=None):
 
1561
        '''
 
1562
        get functional values
 
1563
        '''
 
1564
        result = {}
 
1565
        for stock_picking in self.browse(cr, uid, ids, context=context):
 
1566
            values = {'pack_family_memory_ids':[],
 
1567
                      }
 
1568
            result[stock_picking.id] = values
 
1569
            
 
1570
            # get the corresponding data for pack family memory
 
1571
            data = self.generate_data_from_picking_for_pack_family(cr, uid, [stock_picking.id], context=context)
 
1572
            # create a memory family - no shipment id
 
1573
            created_ids = self.create_pack_families_memory_from_data(cr, uid, data, shipment_id=False, context=context)
 
1574
            values['pack_family_memory_ids'].extend(created_ids)
 
1575
                    
 
1576
        return result
 
1577
    
 
1578
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
1579
        '''
 
1580
        get functional values
 
1581
        '''
 
1582
        result = {}
 
1583
        for stock_picking in self.browse(cr, uid, ids, context=context):
 
1584
            values = {'total_amount': 0.0,
 
1585
                      'currency_id': False,
 
1586
                      'is_dangerous_good': False,
 
1587
                      'is_keep_cool': False,
 
1588
                      'is_narcotic': False,
 
1589
                      'num_of_packs': 0,
 
1590
                      'total_volume': 0.0,
 
1591
                      'total_weight': 0.0,
 
1592
                      #'is_completed': False,
 
1593
                      'overall_qty': 0.0,
 
1594
                      }
 
1595
            result[stock_picking.id] = values
 
1596
            
 
1597
            for family in stock_picking.pack_family_memory_ids:
 
1598
                # number of packs from pack_family
 
1599
                num_of_packs = family.num_of_packs
 
1600
                values['num_of_packs'] += int(num_of_packs)
 
1601
                # total_weight
 
1602
                total_weight = family.total_weight
 
1603
                values['total_weight'] += total_weight
 
1604
                total_volume = family.total_volume
 
1605
                values['total_volume'] += total_volume
 
1606
                
 
1607
            for move in stock_picking.move_lines:
 
1608
                # total amount (float)
 
1609
                total_amount = move.total_amount
 
1610
                values['total_amount'] = total_amount
 
1611
                # currency
 
1612
                values['currency_id'] = move.currency_id and move.currency_id.id or False
 
1613
                # dangerous good
 
1614
                values['is_dangerous_good'] = move.is_dangerous_good
 
1615
                # keep cool - if heat_sensitive_item is True
 
1616
                values['is_keep_cool'] = move.is_keep_cool
 
1617
                # narcotic
 
1618
                values['is_narcotic'] = move.is_narcotic
 
1619
                # overall qty of products in all corresponding stock moves
 
1620
                values['overall_qty'] += move.product_qty
 
1621
                
 
1622
            # completed field - based on the previous_step_ids field, recursive call from picking to draft packing and packing
 
1623
            # - picking checks that the corresponding ppl is completed
 
1624
            # - ppl checks that the corresponding draft packing and packings are completed
 
1625
            # the recursion stops there because packing does not have previous_step_ids values
 
1626
#            completed = stock_picking.state in ('done', 'cancel')
 
1627
#            if completed:
 
1628
#                for next_step in stock_picking.previous_step_ids:
 
1629
#                    if not next_step.is_completed:
 
1630
#                        completed = False
 
1631
#                        break
 
1632
#                    
 
1633
#            values['is_completed'] = completed
 
1634
                    
 
1635
        return result
 
1636
    
 
1637
    def is_completed(self, cr, uid, ids, context=None):
 
1638
        '''
 
1639
        recursive test of completion
 
1640
        - to be applied on picking ticket
 
1641
        
 
1642
        ex:
 
1643
        for picking in draft_picking.backorder_ids:
 
1644
            # take care, is_completed returns a dictionary
 
1645
            if not picking.is_completed()[picking.id]:
 
1646
                ...balbala
 
1647
        
 
1648
        ***BEWARE: RETURNS A DICTIONARY !
 
1649
        '''
 
1650
        result = {}
 
1651
        for stock_picking in self.browse(cr, uid, ids, context=context):
 
1652
            # for debugging
 
1653
            state = stock_picking.state
 
1654
            subtype = stock_picking.subtype
 
1655
            completed = stock_picking.state in ('done', 'cancel')
 
1656
            result[stock_picking.id] = completed
 
1657
            if completed:
 
1658
                for next_step in stock_picking.previous_step_ids:
 
1659
                    if not next_step.is_completed()[next_step.id]:
 
1660
                        completed = False
 
1661
                        result[stock_picking.id] = completed
 
1662
                        break
 
1663
        
 
1664
        return result
 
1665
    
 
1666
    def init(self, cr):
 
1667
        """
 
1668
        Load msf_outgoing_data.xml before self
 
1669
        """
 
1670
        if hasattr(super(stock_picking, self), 'init'):
 
1671
            super(stock_picking, self).init(cr)
 
1672
 
 
1673
        mod_obj = self.pool.get('ir.module.module')
 
1674
        demo = False
 
1675
        mod_id = mod_obj.search(cr, 1, [('name', '=', 'msf_outgoing'),])
 
1676
        if mod_id:
 
1677
            demo = mod_obj.read(cr, 1, mod_id, ['demo'])[0]['demo']
 
1678
 
 
1679
        if demo:
 
1680
            logging.getLogger('init').info('HOOK: module msf_outgoing: loading data/msf_outgoing_data.xml')
 
1681
            pathname = path.join('msf_outgoing', 'data/msf_outgoing_data.xml')
 
1682
            file = tools.file_open(pathname)
 
1683
            tools.convert_xml_import(cr, 'msf_outgoing', file, {}, mode='init', noupdate=False)
 
1684
            
 
1685
    def _qty_search(self, cr, uid, obj, name, args, context=None):
 
1686
        """ Searches Ids of stock picking
 
1687
            @return: Ids of locations
 
1688
        """
 
1689
        if context is None:
 
1690
            context = {}
 
1691
            
 
1692
        stock_pickings = self.pool.get('stock.picking').search(cr, uid, [], context=context)
 
1693
        # result dic
 
1694
        result = {}
 
1695
        for stock_picking in self.browse(cr, uid, stock_pickings, context=context):
 
1696
            result[stock_picking.id] = 0.0
 
1697
            for move in stock_picking.move_lines:
 
1698
                result[stock_picking.id] += move.product_qty
 
1699
        # construct the request
 
1700
        # adapt the operator
 
1701
        op = args[0][1]
 
1702
        if op == '=':
 
1703
            op = '=='
 
1704
        ids = [('id', 'in', [x for x in result.keys() if eval("%s %s %s"%(result[x], op, args[0][2]))])]
 
1705
        return ids
 
1706
    
 
1707
    def _get_picking_ids(self, cr, uid, ids, context=None):
 
1708
        '''
 
1709
        ids represents the ids of stock.move objects for which values have changed
 
1710
        return the list of ids of picking object which need to get their state field updated
 
1711
        
 
1712
        self is stock.move object
 
1713
        '''
 
1714
        result = []
 
1715
        for obj in self.browse(cr, uid, ids, context=context):
 
1716
            if obj.picking_id and obj.picking_id.id not in result:
 
1717
                result.append(obj.picking_id.id)
 
1718
        return result 
 
1719
 
 
1720
    def _get_draft_moves(self, cr, uid, ids, field_name, args, context=None):
 
1721
        '''
 
1722
        Returns True if there is draft moves on Picking Ticket
 
1723
        '''
 
1724
        res = {}
 
1725
 
 
1726
        for pick in self.browse(cr, uid, ids, context=context):
 
1727
            res[pick.id] = False
 
1728
            for move in pick.move_lines:
 
1729
                if move.state == 'draft':
 
1730
                    res[pick.id] = True
 
1731
                    continue
 
1732
 
 
1733
        return res
 
1734
    
 
1735
    _columns = {'flow_type': fields.selection([('full', 'Full'),('quick', 'Quick')], readonly=True, states={'draft': [('readonly', False),],}, string='Flow Type'),
 
1736
                'subtype': fields.selection([('standard', 'Standard'), ('picking', 'Picking'),('ppl', 'PPL'),('packing', 'Packing')], string='Subtype'),
 
1737
                'backorder_ids': fields.one2many('stock.picking', 'backorder_id', string='Backorder ids',),
 
1738
                'previous_step_id': fields.many2one('stock.picking', 'Previous step'),
 
1739
                'previous_step_ids': fields.one2many('stock.picking', 'previous_step_id', string='Previous Step ids',),
 
1740
                'shipment_id': fields.many2one('shipment', string='Shipment'),
 
1741
                'sequence_id': fields.many2one('ir.sequence', 'Picking Ticket Sequence', help="This field contains the information related to the numbering of the picking tickets.", ondelete='cascade'),
 
1742
                'first_shipment_packing_id': fields.many2one('stock.picking', 'Shipment First Step'),
 
1743
                #'pack_family_ids': fields.one2many('pack.family', 'ppl_id', string='Pack Families',),
 
1744
                # attributes for specific packing labels
 
1745
                'ppl_customize_label': fields.many2one('ppl.customize.label', string='Labels Customization',),
 
1746
                # warehouse info (locations) are gathered from here - allow shipment process without sale order
 
1747
                'warehouse_id': fields.many2one('stock.warehouse', string='Warehouse', required=True,),
 
1748
                # flag for converted picking
 
1749
                'converted_to_standard': fields.boolean(string='Converted to Standard'),
 
1750
                # functions
 
1751
                'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals_X'), # old_multi get_vals
 
1752
                'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals'),
 
1753
                'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals'),
 
1754
                'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals'),
 
1755
                'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals'),
 
1756
                'is_dangerous_good': fields.function(_vals_get, method=True, type='boolean', string='Dangerous Good', multi='get_vals'),
 
1757
                'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals'),
 
1758
                'is_narcotic': fields.function(_vals_get, method=True, type='boolean', string='Narcotic', multi='get_vals'),
 
1759
                'overall_qty': fields.function(_vals_get, method=True, fnct_search=_qty_search, type='float', string='Overall Qty', multi='get_vals',
 
1760
                                               store= {'stock.move': (_get_picking_ids, ['product_qty', 'picking_id'], 10),}),
 
1761
                #'is_completed': fields.function(_vals_get, method=True, type='boolean', string='Completed Process', multi='get_vals',),
 
1762
                'pack_family_memory_ids': fields.function(_vals_get_2, method=True, type='one2many', relation='pack.family.memory', string='Memory Families', multi='get_vals_2',),
 
1763
                'description_ppl': fields.char('Description', size=256 ),
 
1764
                'has_draft_moves': fields.function(_get_draft_moves, method=True, type='boolean', string='Has draft moves ?', store=False),
 
1765
                }
 
1766
    _defaults = {'flow_type': 'full',
 
1767
                 'ppl_customize_label': lambda obj, cr, uid, c: len(obj.pool.get('ppl.customize.label').search(cr, uid, [('name', '=', 'Default Label'),], context=c)) and obj.pool.get('ppl.customize.label').search(cr, uid, [('name', '=', 'Default Label'),], context=c)[0] or False,
 
1768
                 'subtype': 'standard',
 
1769
                 'first_shipment_packing_id': False,
 
1770
                 'warehouse_id': lambda obj, cr, uid, c: len(obj.pool.get('stock.warehouse').search(cr, uid, [], context=c)) and obj.pool.get('stock.warehouse').search(cr, uid, [], context=c)[0] or False,
 
1771
                 'converted_to_standard': False,
 
1772
                 }
 
1773
    #_order = 'origin desc, name asc'
 
1774
    _order = 'name desc'
 
1775
 
 
1776
    def onchange_move(self, cr, uid, ids, context=None):
 
1777
        '''
 
1778
        Display or not the 'Confirm' button on Picking Ticket
 
1779
        '''
 
1780
        res = super(stock_picking, self).onchange_move(cr, uid, ids, context=context)
 
1781
 
 
1782
        if ids:
 
1783
            has_draft_moves = self._get_draft_moves(cr, uid, ids, 'has_draft_moves', False)[ids[0]]
 
1784
            res.setdefault('value', {}).update({'has_draft_moves': has_draft_moves})
 
1785
 
 
1786
        return res
 
1787
    
 
1788
    def picking_ticket_data(self, cr, uid, ids, context=None):
 
1789
        '''
 
1790
        generate picking ticket data for report creation
 
1791
        
 
1792
        - sale order line without product: does not work presently
 
1793
        
 
1794
        - many sale order line with same product: stored in different dictionary with line id as key.
 
1795
            so the same product could be displayed many times in the picking ticket according to sale order
 
1796
        
 
1797
        - many stock move with same product: two cases, if from different sale order lines, the above rule applies,
 
1798
            if from the same order line, they will be stored according to prodlot id
 
1799
            
 
1800
        - many stock move with same prodlot (so same product): if same sale order line, the moves will be
 
1801
            stored in the same structure, with global quantity, i.e. this batch for this product for this
 
1802
            sale order line will be displayed only once with summed quantity from concerned stock moves
 
1803
        
 
1804
        [sale_line.id][product_id][prodlot_id]
 
1805
        
 
1806
        other prod lot, not used are added in order that all prod lot are displayed 
 
1807
        
 
1808
        to check, if a move does not come from the sale order line:
 
1809
        stored with line id False, product is relevant, multiple
 
1810
        product for the same 0 line id is possible
 
1811
        '''
 
1812
        result = {}
 
1813
        for stock_picking in self.browse(cr, uid, ids, context=context):
 
1814
            values = {}
 
1815
            result[stock_picking.id] = {'obj': stock_picking,
 
1816
                                        'lines': values,
 
1817
                                        }
 
1818
            for move in stock_picking.move_lines:
 
1819
                if move.product_id: # product is mandatory at stock_move level ;)
 
1820
                    sale_line_id = move.sale_line_id and move.sale_line_id.id or False
 
1821
                    # structure, data is reorganized in order to regroup according to sale order line > product > production lot
 
1822
                    # and to sum the quantities corresponding to different levels because this is impossible within the rml framework
 
1823
                    values \
 
1824
                        .setdefault(sale_line_id, {}) \
 
1825
                        .setdefault('products', {}) \
 
1826
                        .setdefault(move.product_id.id, {}) \
 
1827
                        .setdefault('uoms', {}) \
 
1828
                        .setdefault(move.product_uom.id, {}) \
 
1829
                        .setdefault('lots', {})
 
1830
                        
 
1831
                    # ** sale order line info**
 
1832
                    values[sale_line_id]['obj'] = move.sale_line_id or False
 
1833
                    
 
1834
                    # **uom level info**
 
1835
                    values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['obj'] = move.product_uom
 
1836
                    
 
1837
                    # **prodlot level info**
 
1838
                    if move.prodlot_id:
 
1839
                        values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'].setdefault(move.prodlot_id.id, {})
 
1840
                        # qty corresponding to this production lot
 
1841
                        values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id].setdefault('reserved_qty', 0)
 
1842
                        values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id]['reserved_qty'] += move.product_qty
 
1843
                        # store the object for info retrieval
 
1844
                        values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id]['obj'] = move.prodlot_id
 
1845
                    
 
1846
                    # **product level info**
 
1847
                    # total quantity from STOCK_MOVES for one sale order line (directly for one product)
 
1848
                    # or if not linked to a sale order line, stock move created manually, the line id is False
 
1849
                    # and in this case the product is important
 
1850
                    values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id].setdefault('qty_to_pick_sm', 0)
 
1851
                    values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['qty_to_pick_sm'] += move.product_qty
 
1852
                    # total quantity from SALE_ORDER_LINES, which can be different from the one from stock moves
 
1853
                    # if stock moves have been created manually in the picking, no present in the so, equal to 0 if not linked to an so
 
1854
                    values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id].setdefault('qty_to_pick_so', 0)
 
1855
                    values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['qty_to_pick_so'] += move.sale_line_id and move.sale_line_id.product_uom_qty or 0.0 
 
1856
                    # store the object for info retrieval
 
1857
                    values[sale_line_id]['products'][move.product_id.id]['obj'] = move.product_id
 
1858
                    
 
1859
            # all moves have been treated
 
1860
            # complete the lot lists for each product
 
1861
            for sale_line in values.values():
 
1862
                for product in sale_line['products'].values():
 
1863
                    for uom in product['uoms'].values():
 
1864
                        # loop through all existing production lot for this product - all are taken into account, internal and external
 
1865
                        for lot in product['obj'].prodlot_ids:
 
1866
                            if lot.id not in uom['lots'].keys():
 
1867
                                # the lot is not present, we add it
 
1868
                                uom['lots'][lot.id] = {}
 
1869
                                uom['lots'][lot.id]['obj'] = lot
 
1870
                                # reserved qty is 0 since no stock moves correspond to this lot
 
1871
                                uom['lots'][lot.id]['reserved_qty'] = 0.0
 
1872
                    
 
1873
        return result
 
1874
 
 
1875
    def action_confirm_moves(self, cr, uid, ids, context=None):
 
1876
        '''
 
1877
        Confirm all stock moves of the picking
 
1878
        '''
 
1879
        for pick in self.browse(cr, uid, ids, context=context):
 
1880
            for move in pick.move_lines:
 
1881
                if move.state == 'draft':
 
1882
                    self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context)
 
1883
 
 
1884
        return True
 
1885
    
 
1886
    def create_sequence(self, cr, uid, vals, context=None):
 
1887
        """
 
1888
        Create new entry sequence for every new picking
 
1889
        @param cr: cursor to database
 
1890
        @param user: id of current user
 
1891
        @param ids: list of record ids to be process
 
1892
        @param context: context arguments, like lang, time zone
 
1893
        @return: return a result
 
1894
        
 
1895
        example of name: 'PICK/xxxxx'
 
1896
        example of code: 'picking.xxxxx'
 
1897
        example of prefix: 'PICK'
 
1898
        example of padding: 5
 
1899
        """
 
1900
        seq_pool = self.pool.get('ir.sequence')
 
1901
        seq_typ_pool = self.pool.get('ir.sequence.type')
 
1902
        
 
1903
        default_name = 'Stock Picking'
 
1904
        default_code = 'stock.picking'
 
1905
        default_prefix = ''
 
1906
        default_padding = 0
 
1907
        
 
1908
        if vals is None:
 
1909
            vals = {}
 
1910
 
 
1911
        name = vals.get('name', False)
 
1912
        if not name:
 
1913
            name = default_name
 
1914
        code = vals.get('code', False)
 
1915
        if not code:
 
1916
            code = default_code
 
1917
        prefix = vals.get('prefix', False)
 
1918
        if not prefix:
 
1919
            prefix = default_prefix
 
1920
        padding = vals.get('padding', False)
 
1921
        if not padding:
 
1922
            padding = default_padding
 
1923
 
 
1924
        types = {
 
1925
            'name': name,
 
1926
            'code': code
 
1927
        }
 
1928
        seq_typ_pool.create(cr, uid, types)
 
1929
 
 
1930
        seq = {
 
1931
            'name': name,
 
1932
            'code': code,
 
1933
            'prefix': prefix,
 
1934
            'padding': padding,
 
1935
        }
 
1936
        return seq_pool.create(cr, uid, seq)
 
1937
    
 
1938
    def generate_data_from_picking_for_pack_family(self, cr, uid, pick_ids, object_type='shipment', from_pack=False, to_pack=False, context=None):
 
1939
        '''
 
1940
        generate the data structure from the stock.picking object
 
1941
        
 
1942
        we can limit the generation to certain from/to sequence
 
1943
        
 
1944
        one data for each move_id - here is the difference with data generated from partial
 
1945
        
 
1946
        structure:
 
1947
            {pick_id: {from_pack: {to_pack: {move_id: {data}}}}}
 
1948
            
 
1949
        if the move has a quantity equal to 0, it means that no pack are available,
 
1950
        these moves are therefore not taken into account for the pack families generation
 
1951
        
 
1952
        TODO: integrity constraints
 
1953
        
 
1954
        Note: why the same dictionary is repeated n times for n moves, because
 
1955
        it is directly used when we create the pack families. could be refactored
 
1956
        with one dic per from/to with a 'move_ids' entry
 
1957
        '''
 
1958
        assert bool(from_pack) == bool(to_pack), 'from_pack and to_pack must be either both filled or empty'
 
1959
        result = {}
 
1960
        
 
1961
        
 
1962
        if object_type == 'shipment':
 
1963
            # all moves are taken into account, therefore the back moves are represented
 
1964
            # by done pack families
 
1965
            states = ('cancel')
 
1966
        elif object_type == 'memory':
 
1967
            # done moves are not displayed as pf as we cannot select these packs anymore (they are returned)
 
1968
            states = ('done')
 
1969
        else:
 
1970
            assert False, 'Should not reach this line'
 
1971
        
 
1972
        for pick in self.browse(cr, uid, pick_ids, context=context):
 
1973
            result[pick.id] = {}
 
1974
            for move in pick.move_lines:
 
1975
                if not from_pack or move.from_pack == from_pack:
 
1976
                    if not to_pack or move.to_pack == to_pack:
 
1977
                        # the quantity must be positive and the state depends on the window's type
 
1978
                        if move.product_qty and move.state not in states:
 
1979
                            # subtype == ppl - called from stock picking
 
1980
                            if pick.subtype == 'ppl':
 
1981
                                result[pick.id] \
 
1982
                                    .setdefault(move.from_pack, {}) \
 
1983
                                    .setdefault(move.to_pack, {})[move.id] = {'sale_order_id': pick.sale_id.id,
 
1984
                                                                              'ppl_id': pick.id, # only change between ppl - packing
 
1985
                                                                              'from_pack': move.from_pack,
 
1986
                                                                              'to_pack': move.to_pack,
 
1987
                                                                              'pack_type': move.pack_type.id,
 
1988
                                                                              'length': move.length,
 
1989
                                                                              'width': move.width,
 
1990
                                                                              'height': move.height,
 
1991
                                                                              'weight': move.weight,
 
1992
                                                                              'draft_packing_id': pick.id,
 
1993
                                                                              'description_ppl': pick.description_ppl,
 
1994
                                                                              }
 
1995
                            # subtype == packing - caled from shipment
 
1996
                            elif pick.subtype == 'packing':
 
1997
                                result[pick.id] \
 
1998
                                    .setdefault(move.from_pack, {}) \
 
1999
                                    .setdefault(move.to_pack, {})[move.id] = {'sale_order_id': pick.sale_id.id,
 
2000
                                                                              'ppl_id': pick.previous_step_id.id,
 
2001
                                                                              'from_pack': move.from_pack,
 
2002
                                                                              'to_pack': move.to_pack,
 
2003
                                                                              'pack_type': move.pack_type.id,
 
2004
                                                                              'length': move.length,
 
2005
                                                                              'width': move.width,
 
2006
                                                                              'height': move.height,
 
2007
                                                                              'weight': move.weight,
 
2008
                                                                              'draft_packing_id': pick.id,
 
2009
                                                                              }
 
2010
 
 
2011
                                if object_type != 'memory':
 
2012
                                        result[pick.id][move.from_pack][move.to_pack][move.id]['description_ppl'] = pick.description_ppl
 
2013
 
 
2014
        return result
 
2015
    
 
2016
    def create_pack_families_memory_from_data(self, cr, uid, data, shipment_id, context=None,):
 
2017
        '''
 
2018
        - clear existing pack family memory objects is not necessary thanks to vaccum system
 
2019
        -> in fact cleaning old memory objects reslults in a bug, because when we click on a
 
2020
           pf memory to see it's form view, the shipment view is regenerated (delete the pf)
 
2021
           but then the form view uses the old pf memory id for data and it crashes. Cleaning
 
2022
           of previous pf memory has therefore been removed.
 
2023
        - generate new ones based on data
 
2024
        - return created ids
 
2025
        '''
 
2026
        pf_memory_obj = self.pool.get('pack.family.memory')
 
2027
        # find and delete existing objects
 
2028
        #ids = pf_memory_obj.search(cr, uid, [('shipment_id', '=', shipment_id),], context=context)
 
2029
        #pf_memory_obj.unlink(cr, uid, ids, context=context)
 
2030
        created_ids = []
 
2031
        # create pack family memory
 
2032
        for picking in data.values():
 
2033
            for from_pack in picking.values():
 
2034
                for to_pack in from_pack.values():
 
2035
                    for move_id in to_pack.keys():
 
2036
                        move_data = to_pack[move_id]
 
2037
                    # create corresponding memory object
 
2038
                    move_data.update(name='_name',
 
2039
                                     shipment_id=shipment_id,)
 
2040
                    id = pf_memory_obj.create(cr, uid, move_data, context=context)
 
2041
                    created_ids.append(id)
 
2042
        return created_ids
 
2043
        
 
2044
    def create(self, cr, uid, vals, context=None):
 
2045
        '''
 
2046
        creation of a stock.picking of subtype 'packing' triggers
 
2047
        special behavior :
 
2048
         - creation of corresponding shipment
 
2049
        '''
 
2050
        # For picking ticket from scratch, invoice it !
 
2051
        if not vals.get('sale_id') and not vals.get('purchase_id') and not vals.get('invoice_state') and 'type' in vals and vals['type'] == 'out':
 
2052
            vals['invoice_state'] = '2binvoiced'
 
2053
        # objects
 
2054
        date_tools = self.pool.get('date.tools')
 
2055
        fields_tools = self.pool.get('fields.tools')
 
2056
        db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
 
2057
        db_datetime_format = date_tools.get_db_datetime_format(cr, uid, context=context)
 
2058
        
 
2059
        if context is None:
 
2060
            context = {}
 
2061
        # the action adds subtype in the context depending from which screen it is created
 
2062
        if context.get('picking_screen', False) and not vals.get('name', False):
 
2063
            pick_name = self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket')
 
2064
            vals.update(subtype='picking',
 
2065
                        backorder_id=False,
 
2066
                        name=pick_name,
 
2067
                        flow_type='full',
 
2068
                        )
 
2069
        
 
2070
        if context.get('ppl_screen', False) and not vals.get('name', False):
 
2071
            pick_name = self.pool.get('ir.sequence').get(cr, uid, 'ppl')
 
2072
            vals.update(subtype='ppl',
 
2073
                        backorder_id=False,
 
2074
                        name=pick_name,
 
2075
                        flow_type='full',
 
2076
                        )
 
2077
        # shipment object
 
2078
        shipment_obj = self.pool.get('shipment')
 
2079
        # move object
 
2080
        move_obj = self.pool.get('stock.move')
 
2081
        
 
2082
        # sequence creation
 
2083
        # if draft picking
 
2084
        if 'subtype' in vals and vals['subtype'] == 'picking':
 
2085
            # creation of a new picking ticket
 
2086
            assert 'backorder_id' in vals, 'No backorder_id'
 
2087
            
 
2088
            if not vals['backorder_id']:
 
2089
                # creation of *draft* picking ticket
 
2090
                vals.update(sequence_id=self.create_sequence(cr, uid, {'name':vals['name'],
 
2091
                                                                       'code':vals['name'],
 
2092
                                                                       'prefix':'',
 
2093
                                                                       'padding':2}, context=context))
 
2094
                
 
2095
        if 'subtype' in vals and vals['subtype'] == 'packing':
 
2096
            # creation of a new packing
 
2097
            assert 'backorder_id' in vals, 'No backorder_id'
 
2098
            assert 'shipment_id' in vals, 'No shipment_id'
 
2099
            
 
2100
            if not vals['backorder_id']:
 
2101
                # creation of *draft* picking ticket
 
2102
                vals.update(sequence_id=self.create_sequence(cr, uid, {'name':vals['name'],
 
2103
                                                                       'code':vals['name'],
 
2104
                                                                       'prefix':'',
 
2105
                                                                       'padding':2,
 
2106
                                                                       }, context=context))
 
2107
        
 
2108
        # create packing object
 
2109
        new_packing_id = super(stock_picking, self).create(cr, uid, vals, context=context)
 
2110
        
 
2111
        if 'subtype' in vals and vals['subtype'] == 'packing':
 
2112
            # creation of a new packing
 
2113
            assert 'backorder_id' in vals, 'No backorder_id'
 
2114
            assert 'shipment_id' in vals, 'No shipment_id'
 
2115
            
 
2116
            if vals['backorder_id'] and vals['shipment_id']:
 
2117
                # ship of existing shipment
 
2118
                # no new shipment
 
2119
                # TODO
 
2120
                return new_packing_id
 
2121
            
 
2122
            if vals['backorder_id'] and not vals['shipment_id']:
 
2123
                # data from do_create_shipment method
 
2124
                assert 'partial_datas_shipment' in context, 'Missing partial_datas_shipment'
 
2125
                assert 'draft_shipment_id' in context, 'Missing draft_shipment_id'
 
2126
                assert 'draft_packing_id' in context, 'Missing draft_packing_id'
 
2127
                assert 'shipment_id' in context, 'Missing shipment_id'
 
2128
                draft_shipment_id = context['draft_shipment_id']
 
2129
                draft_packing_id = context['draft_packing_id']
 
2130
                data = context['partial_datas_shipment'][draft_shipment_id][draft_packing_id]
 
2131
                shipment_id = context['shipment_id']
 
2132
                # We have a backorder_id, no shipment_id
 
2133
                # -> we have just created a shipment
 
2134
                # the created packing object has no stock_move
 
2135
                # - we create the sock move from the data in context
 
2136
                # - if no shipment in context, create a new shipment object
 
2137
                # - generate the data from the new picking object
 
2138
                # - create the pack families
 
2139
                for from_pack in data:
 
2140
                    for to_pack in data[from_pack]:
 
2141
                        # total number of packs
 
2142
                        total_num = to_pack - from_pack + 1
 
2143
                        # number of selected packs to ship
 
2144
                        # note: when the data is generated, lines without selected_number are not kept, so we have nothing to check here
 
2145
                        selected_number = data[from_pack][to_pack][0]['selected_number']
 
2146
                        # we take the packs with the highest numbers
 
2147
                        # new moves
 
2148
                        selected_from_pack = to_pack - selected_number + 1
 
2149
                        selected_to_pack = to_pack
 
2150
                        # update initial moves
 
2151
                        if selected_number == total_num:
 
2152
                            # if all packs have been selected, from/to are set to 0
 
2153
                            initial_from_pack = 0
 
2154
                            initial_to_pack = 0
 
2155
                        else:
 
2156
                            initial_from_pack = from_pack
 
2157
                            initial_to_pack = to_pack - selected_number
 
2158
                        
 
2159
                        # find the corresponding moves
 
2160
                        moves_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_packing_id),
 
2161
                                                              ('from_pack', '=', from_pack),
 
2162
                                                              ('to_pack', '=', to_pack),], context=context)
 
2163
                        
 
2164
                        for move in move_obj.browse(cr, uid, moves_ids, context=context):
 
2165
                            # we compute the selected quantity
 
2166
                            selected_qty = move.qty_per_pack * selected_number
 
2167
                            # create the new move - store the back move from draft **packing** object
 
2168
                            new_move = move_obj.copy(cr, uid, move.id, {'picking_id': new_packing_id,
 
2169
                                                                        'product_qty': selected_qty,
 
2170
                                                                        'from_pack': selected_from_pack,
 
2171
                                                                        'to_pack': selected_to_pack,
 
2172
                                                                        'backmove_packing_id': move.id,}, context=context)
 
2173
                            
 
2174
                            # update corresponding initial move
 
2175
                            initial_qty = move.product_qty
 
2176
                            initial_qty = max(initial_qty - selected_qty, 0)
 
2177
                            # if all packs have been selected, from/to have been set to 0
 
2178
                            # update the original move object - the corresponding original shipment (draft)
 
2179
                            # is automatically updated generically in the write method
 
2180
                            move_obj.write(cr, uid, [move.id], {'product_qty': initial_qty,
 
2181
                                                                'from_pack': initial_from_pack,
 
2182
                                                                'to_pack': initial_to_pack}, context=context)
 
2183
            
 
2184
            if not vals['backorder_id']:
 
2185
                # creation of packing after ppl validation
 
2186
                # find an existing shipment or create one - depends on new pick state
 
2187
                shipment_ids = shipment_obj.search(cr, uid, [('state', '=', 'draft'), ('address_id', '=', vals['address_id'])], context=context)
 
2188
                # only one 'draft' shipment should be available
 
2189
                assert len(shipment_ids) in (0, 1), 'Only one draft shipment should be available for a given address at a time - %s'%len(shipment_ids)
 
2190
                # get rts of corresponding sale order
 
2191
                sale_id = self.read(cr, uid, [new_packing_id], ['sale_id'], context=context)
 
2192
                sale_id = sale_id[0]['sale_id']
 
2193
                if sale_id:
 
2194
                    sale_id = sale_id[0]
 
2195
                    # today
 
2196
                    today = time.strftime(db_datetime_format)
 
2197
                    rts = self.pool.get('sale.order').read(cr, uid, [sale_id], ['ready_to_ship_date'], context=context)[0]['ready_to_ship_date']
 
2198
                else:
 
2199
                    rts = date.today().strftime(db_date_format)
 
2200
                # rts + shipment lt
 
2201
                shipment_lt = fields_tools.get_field_from_company(cr, uid, object=self._name, field='shipment_lead_time', context=context)
 
2202
                rts_obj = datetime.strptime(rts, db_date_format)
 
2203
                rts = rts_obj + relativedelta(days=shipment_lt or 0)
 
2204
                rts = rts.strftime(db_date_format)
 
2205
                
 
2206
                if not len(shipment_ids):
 
2207
                    # no shipment, create one - no need to specify the state, it's a function
 
2208
                    name = self.pool.get('ir.sequence').get(cr, uid, 'shipment')
 
2209
                    addr = self.pool.get('res.partner.address').browse(cr, uid, vals['address_id'], context=context)
 
2210
                    partner_id = addr.partner_id and addr.partner_id.id or False
 
2211
                    values = {'name': name,
 
2212
                              'address_id': vals['address_id'],
 
2213
                              'partner_id2': partner_id,
 
2214
                              'shipment_expected_date': rts,
 
2215
                              'shipment_actual_date': rts,
 
2216
                              'sequence_id': self.create_sequence(cr, uid, {'name':name,
 
2217
                                                                            'code':name,
 
2218
                                                                            'prefix':'',
 
2219
                                                                            'padding':2}, context=context)}
 
2220
                    
 
2221
                    shipment_id = shipment_obj.create(cr, uid, values, context=context)
 
2222
                    shipment_obj.log(cr, uid, shipment_id, _('The new Draft Shipment %s has been created.')%(name,))
 
2223
                else:
 
2224
                    shipment_id = shipment_ids[0]
 
2225
                    shipment = shipment_obj.browse(cr, uid, shipment_id, context=context)
 
2226
                    # if expected ship date of shipment is greater than rts, update shipment_expected_date and shipment_actual_date
 
2227
                    shipment_expected = datetime.strptime(shipment.shipment_expected_date, db_datetime_format)
 
2228
                    if rts_obj < shipment_expected:
 
2229
                        shipment.write({'shipment_expected_date': rts, 'shipment_actual_date': rts,}, context=context)
 
2230
                    shipment_name = shipment.name
 
2231
                    shipment_obj.log(cr, uid, shipment_id, _('The ppl has been added to the existing Draft Shipment %s.')%(shipment_name,))
 
2232
            
 
2233
            # update the new pick with shipment_id
 
2234
            self.write(cr, uid, [new_packing_id], {'shipment_id': shipment_id}, context=context)
 
2235
            
 
2236
        return new_packing_id
 
2237
 
 
2238
    def _hook_action_assign_raise_exception(self, cr, uid, ids, context=None, *args, **kwargs):
 
2239
        '''
 
2240
        Please copy this to your module's method also.
 
2241
        This hook belongs to the action_assign method from stock>stock.py>stock_picking class
 
2242
        
 
2243
        - allow to choose wether or not an exception should be raised in case of no stock move
 
2244
        '''
 
2245
        res = super(stock_picking, self)._hook_action_assign_raise_exception(cr, uid, ids, context=context, *args, **kwargs)
 
2246
        return res and False
 
2247
    
 
2248
    def _hook_log_picking_modify_message(self, cr, uid, ids, context=None, *args, **kwargs):
 
2249
        '''
 
2250
        stock>stock.py>log_picking
 
2251
        update the message to be displayed by the function
 
2252
        '''
 
2253
        pick = kwargs['pick']
 
2254
        message = kwargs['message']
 
2255
        # if the picking is converted to standard, and state is confirmed
 
2256
        if pick.converted_to_standard and pick.state == 'confirmed':
 
2257
            return 'The Preparation Picking has been converted to simple Out. ' + message
 
2258
        if pick.type == 'out' and pick.subtype == 'picking':
 
2259
            kwargs['message'] = message.replace('Delivery Order', 'Picking Ticket')
 
2260
        elif pick.type == 'out' and pick.subtype == 'packing':
 
2261
            kwargs['message'] = message.replace('Delivery Order', 'Packing List')
 
2262
        elif pick.type == 'out' and pick.subtype == 'ppl':
 
2263
            kwargs['message'] = message.replace('Delivery Order', 'Pre-Packing List')
 
2264
        return super(stock_picking, self)._hook_log_picking_modify_message(cr, uid, ids, context, *args, **kwargs)
 
2265
    
 
2266
    def convert_to_standard(self, cr, uid, ids, context=None):
 
2267
        '''
 
2268
        check of back orders exists, if not, convert to standard: change subtype to standard, and trigger workflow
 
2269
        
 
2270
        only one picking object at a time
 
2271
        '''
 
2272
        if not context:
 
2273
            context = {}
 
2274
        # objects
 
2275
        date_tools = self.pool.get('date.tools')
 
2276
        fields_tools = self.pool.get('fields.tools')
 
2277
        db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
 
2278
        for obj in self.browse(cr, uid, ids, context=context):
 
2279
            # the convert function should only be called on draft picking ticket
 
2280
            assert obj.subtype == 'picking' and obj.state == 'draft', 'the convert function should only be called on draft picking ticket objects'
 
2281
            if self.has_picking_ticket_in_progress(cr, uid, [obj.id], context=context)[obj.id]:
 
2282
                    raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try again.'))
 
2283
            
 
2284
            # log a message concerning the conversion
 
2285
            new_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
 
2286
            self.log(cr, uid, obj.id, _('The Preparation Picking (%s) has been converted to simple Out (%s).')%(obj.name, new_name))
 
2287
            # change subtype and name
 
2288
            obj.write({'name': new_name,
 
2289
                       'subtype': 'standard',
 
2290
                       'converted_to_standard': True,
 
2291
                       }, context=context)
 
2292
            # all destination location of the stock moves must be output location of warehouse - lot_output_id
 
2293
            # if corresponding sale order, date and date_expected are updated to rts + shipment lt
 
2294
            for move in obj.move_lines:
 
2295
                # was previously set to confirmed/assigned, otherwise, when we confirm the stock picking,
 
2296
                # using draft_force_assign, the moves are not treated because not in draft
 
2297
                # and the corresponding chain location on location_dest_id was not computed
 
2298
                # we therefore set them back in draft state before treatment
 
2299
                if move.product_qty == 0.0:
 
2300
                    vals = {'state': 'done'}
 
2301
                else:
 
2302
                    vals = {'state': 'draft'}
 
2303
                # If the move comes from a DPO, don't change the destination location
 
2304
                if not move.dpo_id:
 
2305
                    vals.update({'location_dest_id': obj.warehouse_id.lot_output_id.id})
 
2306
 
 
2307
                if obj.sale_id:
 
2308
                    # compute date
 
2309
                    shipment_lt = fields_tools.get_field_from_company(cr, uid, object=self._name, field='shipment_lead_time', context=context)
 
2310
                    rts = datetime.strptime(obj.sale_id.ready_to_ship_date, db_date_format)
 
2311
                    rts = rts + relativedelta(days=shipment_lt or 0)
 
2312
                    rts = rts.strftime(db_date_format)
 
2313
                    vals.update({'date': rts, 'date_expected': rts, 'state': 'draft'})
 
2314
                move.write(vals, context=context)
 
2315
                if move.product_qty == 0.00:
 
2316
                    move.action_done(context=context)
 
2317
 
 
2318
 
 
2319
            # trigger workflow (confirm picking)
 
2320
            self.draft_force_assign(cr, uid, [obj.id])
 
2321
            # check availability
 
2322
            self.action_assign(cr, uid, [obj.id], context=context)
 
2323
        
 
2324
            # TODO which behavior
 
2325
            data_obj = self.pool.get('ir.model.data')
 
2326
            view_id = data_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
 
2327
            view_id = view_id and view_id[1] or False
 
2328
            context.update({'picking_type': 'delivery_order'})
 
2329
            return {'name':_("Delivery Orders"),
 
2330
                    'view_mode': 'form,tree',
 
2331
                    'view_id': [view_id],
 
2332
                    'view_type': 'form',
 
2333
                    'res_model': 'stock.picking',
 
2334
                    'res_id': obj.id,
 
2335
                    'type': 'ir.actions.act_window',
 
2336
                    'target': 'crush',
 
2337
                    'context': context,
 
2338
                    }
 
2339
    
 
2340
    def create_picking(self, cr, uid, ids, context=None):
 
2341
        '''
 
2342
        open the wizard to create (partial) picking tickets
 
2343
        '''
 
2344
        # we need the context for the wizard switch
 
2345
        if context is None:
 
2346
            context = {}
 
2347
        
 
2348
        # data
 
2349
        name = _("Create Picking Ticket")
 
2350
        model = 'create.picking'
 
2351
        step = 'create'
 
2352
        wiz_obj = self.pool.get('wizard')
 
2353
        # open the selected wizard
 
2354
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
2355
 
 
2356
    def do_create_picking_first_hook(self, cr, uid, ids, context, *args, **kwargs):
 
2357
        '''
 
2358
        hook to update new_move data. Originally: to complete msf_cross_docking module
 
2359
        '''
 
2360
        values = kwargs.get('values')
 
2361
        assert values is not None, 'missing defaults'
 
2362
        
 
2363
        return values
 
2364
 
 
2365
    def do_create_picking(self, cr, uid, ids, context=None):
 
2366
        '''
 
2367
        create the picking ticket from selected stock moves
 
2368
        '''
 
2369
        if not context:
 
2370
            context = {}
 
2371
        assert context, 'context is not defined'
 
2372
        assert 'partial_datas' in context, 'partial datas not present in context'
 
2373
        partial_datas = context['partial_datas']
 
2374
        
 
2375
        # stock move object
 
2376
        move_obj = self.pool.get('stock.move')
 
2377
        
 
2378
        for pick in self.browse(cr, uid, ids, context=context):
 
2379
            # create the new picking object
 
2380
            # a sequence for each draft picking ticket is used for the picking ticket
 
2381
            sequence = pick.sequence_id
 
2382
            ticket_number = sequence.get_id(test='id', context=context)
 
2383
            new_pick_id = self.copy(cr, uid, pick.id, {'name': (pick.name or 'NoName/000') + '-' + ticket_number,
 
2384
                                                       'backorder_id': pick.id,
 
2385
                                                       'move_lines': []}, context=dict(context, allow_copy=True,))
 
2386
            # create stock moves corresponding to partial datas
 
2387
            # for now, each new line from the wizard corresponds to a new stock.move
 
2388
            # it could be interesting to regroup according to production lot/asset id
 
2389
            move_ids = partial_datas[pick.id].keys()
 
2390
            for move in move_obj.browse(cr, uid, move_ids, context=context):
 
2391
                # qty selected
 
2392
                count = 0
 
2393
                # initial qty
 
2394
                initial_qty = move.product_qty
 
2395
                for partial in partial_datas[pick.id][move.id]:
 
2396
                    # integrity check
 
2397
                    assert partial['product_id'] == move.product_id.id, 'product id is wrong, %s - %s'%(partial['product_id'], move.product_id.id)
 
2398
                    assert partial['product_uom'] == move.product_uom.id, 'product uom is wrong, %s - %s'%(partial['product_uom'], move.product_uom.id)
 
2399
                    # the quantity
 
2400
                    count = count + partial['product_qty']
 
2401
                    # copy the stock move and set the quantity
 
2402
                    values = {'picking_id': new_pick_id,
 
2403
                              'product_qty': partial['product_qty'],
 
2404
                              'product_uos_qty': partial['product_qty'],
 
2405
                              'prodlot_id': partial['prodlot_id'],
 
2406
                              'asset_id': partial['asset_id'],
 
2407
                              'composition_list_id': partial['composition_list_id'],
 
2408
                              'backmove_id': move.id}
 
2409
                    #add hook
 
2410
                    values = self.do_create_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
 
2411
                    new_move = move_obj.copy(cr, uid, move.id, values, context=dict(context, keepLineNumber=True))
 
2412
                    
 
2413
                # decrement the initial move, cannot be less than zero and mark the stock move as processed - will not be updated by delivery_mech anymore
 
2414
                initial_qty = max(initial_qty - count, 0)
 
2415
                move_obj.write(cr, uid, [move.id], {'product_qty': initial_qty, 'product_uos_qty': initial_qty, 'processed_stock_move': True}, context=context)
 
2416
                
 
2417
            # confirm the new picking ticket
 
2418
            wf_service = netsvc.LocalService("workflow")
 
2419
            wf_service.trg_validate(uid, 'stock.picking', new_pick_id, 'button_confirm', cr)
 
2420
            # we force availability
 
2421
            self.force_assign(cr, uid, [new_pick_id])
 
2422
        
 
2423
        # TODO which behavior
 
2424
        #return {'type': 'ir.actions.act_window_close'}
 
2425
        data_obj = self.pool.get('ir.model.data')
 
2426
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
 
2427
        view_id = view_id and view_id[1] or False
 
2428
        context.update({'picking_type': 'picking_ticket', 'picking_screen': True})
 
2429
        return {'name':_("Picking Ticket"),
 
2430
                'view_mode': 'form,tree',
 
2431
                'view_id': [view_id],
 
2432
                'view_type': 'form',
 
2433
                'res_model': 'stock.picking',
 
2434
                'res_id': new_pick_id,
 
2435
                'type': 'ir.actions.act_window',
 
2436
                'target': 'crush',
 
2437
                'context': context,
 
2438
                }
 
2439
        
 
2440
    def validate_picking(self, cr, uid, ids, context=None):
 
2441
        '''
 
2442
        validate the picking ticket
 
2443
        '''
 
2444
        # we need the context for the wizard switch
 
2445
        if context is None:
 
2446
            context = {}
 
2447
            
 
2448
        # data
 
2449
        name = _("Validate Picking Ticket")
 
2450
        model = 'create.picking'
 
2451
        step = 'validate'
 
2452
        wiz_obj = self.pool.get('wizard')
 
2453
        # open the selected wizard
 
2454
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
2455
 
 
2456
    def do_validate_picking_first_hook(self, cr, uid, ids, context, *args, **kwargs):
 
2457
        '''
 
2458
        hook to update new_move data. Originally: to complete msf_cross_docking module
 
2459
        '''
 
2460
        values = kwargs.get('values')
 
2461
        assert values is not None, 'missing defaults'
 
2462
        
 
2463
        return values
 
2464
 
 
2465
    def do_validate_picking(self, cr, uid, ids, context=None):
 
2466
        '''
 
2467
        validate the picking ticket from selected stock moves
 
2468
        
 
2469
        move here the logic of validate picking
 
2470
        available for picking loop
 
2471
        '''
 
2472
        if not context:
 
2473
            context = {}
 
2474
        assert context, 'context is not defined'
 
2475
        assert 'partial_datas' in context, 'partial datas not present in context'
 
2476
        partial_datas = context['partial_datas']
 
2477
        
 
2478
        # objects
 
2479
        date_tools = self.pool.get('date.tools')
 
2480
        db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
 
2481
        today = time.strftime(db_date_format)
 
2482
        
 
2483
        # stock move object
 
2484
        move_obj = self.pool.get('stock.move')
 
2485
        # create picking object
 
2486
        create_picking_obj = self.pool.get('create.picking')
 
2487
        
 
2488
        new_ppl = False
 
2489
        for pick in self.browse(cr, uid, ids, context=context):
 
2490
            # create stock moves corresponding to partial datas
 
2491
            move_ids = partial_datas[pick.id].keys()
 
2492
            for move in move_obj.browse(cr, uid, move_ids, context=context):
 
2493
                # qty selected
 
2494
                count = 0
 
2495
                # flag to update the first move - if split was performed during the validation, new stock moves are created
 
2496
                first = True
 
2497
                # initial qty
 
2498
                initial_qty = move.product_qty
 
2499
                for partial in partial_datas[pick.id][move.id]:
 
2500
                    # integrity check
 
2501
                    assert partial['product_id'] == move.product_id.id, 'product id is wrong, %s - %s'%(partial['product_id'], move.product_id.id)
 
2502
                    assert partial['product_uom'] == move.product_uom.id, 'product uom is wrong, %s - %s'%(partial['product_uom'], move.product_uom.id)
 
2503
                    # the quantity
 
2504
                    count = count + partial['product_qty']
 
2505
                    if first:
 
2506
                        first = False
 
2507
                        # update existing move
 
2508
                        values = {'product_qty': partial['product_qty'],
 
2509
                                  'product_uos_qty': partial['product_qty'],
 
2510
                                  'prodlot_id': partial['prodlot_id'],
 
2511
                                  'composition_list_id': partial['composition_list_id'],
 
2512
                                  'asset_id': partial['asset_id']}
 
2513
                        values = self.do_validate_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
 
2514
                        move_obj.write(cr, uid, [move.id], values, context=context)
 
2515
                    else:
 
2516
                        # split happend during the validation
 
2517
                        # copy the stock move and set the quantity
 
2518
                        values = {'state': 'assigned',
 
2519
                                  'product_qty': partial['product_qty'],
 
2520
                                  'product_uos_qty': partial['product_qty'],
 
2521
                                  'prodlot_id': partial['prodlot_id'],
 
2522
                                  'composition_list_id': partial['composition_list_id'],
 
2523
                                  'asset_id': partial['asset_id']}
 
2524
                        values = self.do_validate_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
 
2525
                        new_move = move_obj.copy(cr, uid, move.id, values, context=dict(context, keepLineNumber=True))
 
2526
                # decrement the initial move, cannot be less than zero
 
2527
                diff_qty = initial_qty - count
 
2528
                # the quantity after the validation does not correspond to the picking ticket quantity
 
2529
                # the difference is written back to draft picking ticket
 
2530
                # is positive if some qty was removed during the validation -> draft qty is increased
 
2531
                # is negative if some qty was added during the validation -> draft qty is decreased
 
2532
                if diff_qty != 0:
 
2533
                    # original move from the draft picking ticket which will be updated
 
2534
                    original_move = move.backmove_id
 
2535
                    backorder_qty = move_obj.read(cr, uid, [original_move.id], ['product_qty'], context=context)[0]['product_qty']
 
2536
                    backorder_qty = max(backorder_qty + diff_qty, 0)
 
2537
                    move_obj.write(cr, uid, [original_move.id], {'product_qty': backorder_qty}, context=context)
 
2538
 
 
2539
            # create the new ppl object
 
2540
            ppl_number = pick.name.split("/")[1]
 
2541
            # we want the copy to keep the production lot reference from picking ticket to pre-packing list
 
2542
            new_ppl_id = self.copy(cr, uid, pick.id, {'name': 'PPL/' + ppl_number,
 
2543
                                                      'subtype': 'ppl',
 
2544
                                                      'previous_step_id': pick.id,
 
2545
                                                      'backorder_id': False}, context=dict(context, keep_prodlot=True, allow_copy=True, keepLineNumber=True))
 
2546
            new_ppl = self.browse(cr, uid, new_ppl_id, context=context)
 
2547
            # update locations of stock moves - if the move quantity is equal to zero, the stock move is removed
 
2548
            for move in new_ppl.move_lines:
 
2549
                if move.product_qty:
 
2550
                    move_obj.write(cr, uid, [move.id], {'initial_location': move.location_id.id,
 
2551
                                                        'location_id': move.location_dest_id.id,
 
2552
                                                        'location_dest_id': new_ppl.warehouse_id.lot_dispatch_id.id,
 
2553
                                                        'date': today,
 
2554
                                                        'date_expected': today,}, context=context)
 
2555
                else:
 
2556
                    move_obj.unlink(cr, uid, [move.id], context=dict(context, skipResequencing=True))
 
2557
            
 
2558
            wf_service = netsvc.LocalService("workflow")
 
2559
            wf_service.trg_validate(uid, 'stock.picking', new_ppl_id, 'button_confirm', cr)
 
2560
            # simulate check assign button, as stock move must be available
 
2561
            self.force_assign(cr, uid, [new_ppl_id])
 
2562
            # trigger standard workflow for validated picking ticket
 
2563
            self.action_move(cr, uid, [pick.id])
 
2564
            wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
 
2565
            
 
2566
            # if the flow type is in quick mode, we perform the ppl steps automatically
 
2567
            if pick.flow_type == 'quick':
 
2568
                create_picking_obj.quick_mode(cr, uid, new_ppl, context=context)
 
2569
 
 
2570
        # TODO which behavior
 
2571
        #return {'type': 'ir.actions.act_window_close'}
 
2572
        data_obj = self.pool.get('ir.model.data')
 
2573
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_form')
 
2574
        view_id = view_id and view_id[1] or False
 
2575
        context.update({'picking_type': 'picking_ticket', 'ppl_screen': True})
 
2576
        return {'name':_("Pre-Packing List"),
 
2577
                'view_mode': 'form,tree',
 
2578
                'view_id': [view_id],
 
2579
                'view_type': 'form',
 
2580
                'res_model': 'stock.picking',
 
2581
                'res_id': new_ppl and new_ppl.id or False,
 
2582
                'type': 'ir.actions.act_window',
 
2583
                'target': 'crush',
 
2584
                'context': context,
 
2585
                }
 
2586
 
 
2587
    def ppl(self, cr, uid, ids, context=None):
 
2588
        '''
 
2589
        pack the ppl - open the ppl step1 wizard
 
2590
        '''
 
2591
        # we need the context for the wizard switch
 
2592
        if context is None:
 
2593
            context = {}
 
2594
            
 
2595
        # data
 
2596
        name = _("PPL Information - step1")
 
2597
        model = 'create.picking'
 
2598
        step = 'ppl1'
 
2599
        wiz_obj = self.pool.get('wizard')
 
2600
        # open the selected wizard
 
2601
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
2602
        
 
2603
    def do_ppl1(self, cr, uid, ids, context=None):
 
2604
        '''
 
2605
        - receives generated data from ppl in context
 
2606
        - call action to ppl2 step with partial_datas_ppl1 in context
 
2607
        - ids are the picking ids
 
2608
        '''
 
2609
        # we need the context for the wizard switch
 
2610
        assert context, 'No context defined'
 
2611
            
 
2612
        # data
 
2613
        name = _("PPL Information - step2")
 
2614
        model = 'create.picking'
 
2615
        step = 'ppl2'
 
2616
        wiz_obj = self.pool.get('wizard')
 
2617
        # open the selected wizard
 
2618
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
2619
        
 
2620
    def do_ppl2(self, cr, uid, ids, context=None):
 
2621
        '''
 
2622
        finalize the ppl logic
 
2623
        '''
 
2624
        # integrity check
 
2625
        assert context, 'context not defined'
 
2626
        assert 'partial_datas_ppl1' in context, 'partial_datas_ppl1 no defined in context'
 
2627
        
 
2628
        move_obj = self.pool.get('stock.move')
 
2629
        wf_service = netsvc.LocalService("workflow")
 
2630
        # data from wizard
 
2631
        partial_datas_ppl = context['partial_datas_ppl1']
 
2632
        # picking ids from ids must be equal to picking ids from partial datas
 
2633
        assert set(ids) == set(partial_datas_ppl.keys()), 'picking ids from ids and partial do not match'
 
2634
        
 
2635
        # update existing stock moves - create new one if split occurred
 
2636
        # for each pick
 
2637
        for pick in self.browse(cr, uid, ids, context=context):
 
2638
            # integrity check on move_ids - moves ids from picking and partial must be the same
 
2639
            # dont take into account done moves, which represents returned products
 
2640
            from_pick = [move.id for move in pick.move_lines if move.state in ('confirmed', 'assigned')]
 
2641
            from_partial = []
 
2642
            # the list of updated stock.moves
 
2643
            # if a stock.move is updated, the next time a new move is created
 
2644
            updated = {}
 
2645
            # load moves data
 
2646
            # browse returns a list of browse object in the same order as from_partial
 
2647
            browse_moves = move_obj.browse(cr, uid, from_pick, context=context)
 
2648
            moves = dict(zip(from_pick, browse_moves))
 
2649
            # loop through data
 
2650
            for from_pack in partial_datas_ppl[pick.id]:
 
2651
                for to_pack in partial_datas_ppl[pick.id][from_pack]:
 
2652
                    for move in partial_datas_ppl[pick.id][from_pack][to_pack]:
 
2653
                        # integrity check
 
2654
                        from_partial.append(move)
 
2655
                        for partial in partial_datas_ppl[pick.id][from_pack][to_pack][move]:
 
2656
                            # {'asset_id': False, 'weight': False, 'product_id': 77, 'product_uom': 1, 'pack_type': False, 'length': False, 'to_pack': 1, 'height': False, 'from_pack': 1, 'prodlot_id': False, 'qty_per_pack': 18.0, 'product_qty': 18.0, 'width': False, 'move_id': 179}
 
2657
                            # integrity check
 
2658
                            assert partial['product_id'] == moves[move].product_id.id
 
2659
                            assert partial['asset_id'] == moves[move].asset_id.id
 
2660
                            assert partial['composition_list_id'] == moves[move].composition_list_id.id
 
2661
                            assert partial['product_uom'] == moves[move].product_uom.id
 
2662
                            assert partial['prodlot_id'] == moves[move].prodlot_id.id
 
2663
                            # dictionary of new values, used for creation or update
 
2664
                            # - qty_per_pack is a function at stock move level
 
2665
                            fields = ['product_qty', 'from_pack', 'to_pack', 'pack_type', 'length', 'width', 'height', 'weight']
 
2666
                            values = dict(zip(fields, [partial["%s"%x] for x in fields]))
 
2667
                            
 
2668
                            if move in updated:
 
2669
                                # if already updated, we create a new stock.move
 
2670
                                updated[move]['partial_qty'] += partial['product_qty']
 
2671
                                # force state to 'assigned'
 
2672
                                values.update(state='assigned')
 
2673
                                # copy stock.move with new product_qty, qty_per_pack. from_pack, to_pack, pack_type, length, width, height, weight
 
2674
                                new_move = move_obj.copy(cr, uid, move, values, context=context)
 
2675
                                # Need to change the locations after the copy, because the create of a new stock move with
 
2676
                                # non-stockable product force the locations
 
2677
                                move_obj.write(cr, uid, [new_move], {'location_id': moves[move].location_id.id,
 
2678
                                                                     'location_dest_id': moves[move].location_dest_id.id}, context=context)
 
2679
                            else:
 
2680
                                # update the existing stock move
 
2681
                                updated[move] = {'initial': moves[move].product_qty, 'partial_qty': partial['product_qty']}
 
2682
                                move_obj.write(cr, uid, [move], values, context=context)
 
2683
        
 
2684
            # integrity check - all moves are treated and no more
 
2685
            assert set(from_pick) == set(from_partial), 'move_ids are not equal pick:%s - partial:%s'%(set(from_pick), set(from_partial))
 
2686
            # quantities are right
 
2687
            assert all([updated[m]['initial'] == updated[m]['partial_qty'] for m in updated.keys()]), 'initial quantity is not equal to the sum of partial quantities (%s).'%(updated)
 
2688
            # copy to 'packing' stock.picking
 
2689
            # draft shipment is automatically created or updated if a shipment already
 
2690
            pack_number = pick.name.split("/")[1]
 
2691
            new_packing_id = self.copy(cr, uid, pick.id, {'name': 'PACK/' + pack_number,
 
2692
                                                          'subtype': 'packing',
 
2693
                                                          'previous_step_id': pick.id,
 
2694
                                                          'backorder_id': False,
 
2695
                                                          'shipment_id': False}, context=dict(context, keep_prodlot=True, allow_copy=True,))
 
2696
 
 
2697
            self.write(cr, uid, [new_packing_id], {'origin': pick.origin}, context=context)
 
2698
            # update locations of stock moves and state as the picking stay at 'draft' state.
 
2699
            # if return move have been done in previous ppl step, we remove the corresponding copied move (criteria: qty_per_pack == 0)
 
2700
            new_packing = self.browse(cr, uid, new_packing_id, context=context)
 
2701
            for move in new_packing.move_lines:
 
2702
                if move.qty_per_pack == 0:
 
2703
                    move_obj.unlink(cr, uid, [move.id], context=context)
 
2704
                else:
 
2705
                    move.write({'state': 'assigned',
 
2706
                                'location_id': new_packing.warehouse_id.lot_dispatch_id.id,
 
2707
                                'location_dest_id': new_packing.warehouse_id.lot_distribution_id.id}, context=context)
 
2708
            
 
2709
            # trigger standard workflow
 
2710
            self.action_move(cr, uid, [pick.id])
 
2711
            wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
 
2712
        
 
2713
        # TODO which behavior
 
2714
        #return {'type': 'ir.actions.act_window_close'}
 
2715
        data_obj = self.pool.get('ir.model.data')
 
2716
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
 
2717
        view_id = view_id and view_id[1] or False
 
2718
        return {'name':_("Shipment"),
 
2719
                'view_mode': 'form,tree',
 
2720
                'view_id': [view_id],
 
2721
                'view_type': 'form',
 
2722
                'res_model': 'shipment',
 
2723
                'res_id': new_packing.shipment_id.id,
 
2724
                'type': 'ir.actions.act_window',
 
2725
                'target': 'crush',
 
2726
                'context': context,
 
2727
                }
 
2728
    
 
2729
    def return_products(self, cr, uid, ids, context=None):
 
2730
        '''
 
2731
        open the return products wizard
 
2732
        '''
 
2733
        # we need the context
 
2734
        if context is None:
 
2735
            context = {}
 
2736
            
 
2737
        # data
 
2738
        name = _("Return Products")
 
2739
        model = 'create.picking'
 
2740
        step = 'returnproducts'
 
2741
        wiz_obj = self.pool.get('wizard')
 
2742
        # open the selected wizard
 
2743
        return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
 
2744
    
 
2745
    def do_return_products(self, cr, uid, ids, context=None):
 
2746
        '''
 
2747
        - update the ppl
 
2748
        - update the draft picking ticket
 
2749
        - create the back move
 
2750
        '''
 
2751
        if not context:
 
2752
            context = {}
 
2753
        # integrity check
 
2754
        assert context, 'context not defined'
 
2755
        assert 'partial_datas' in context, 'partial_datas no defined in context'
 
2756
        partial_datas = context['partial_datas']
 
2757
        
 
2758
        move_obj = self.pool.get('stock.move')
 
2759
        wf_service = netsvc.LocalService("workflow")
 
2760
       
 
2761
        draft_picking_id = False
 
2762
        for picking in self.browse(cr, uid, ids, context=context):
 
2763
            # for each picking
 
2764
            # corresponding draft picking ticket
 
2765
            draft_picking_id = picking.previous_step_id.backorder_id.id
 
2766
            
 
2767
            for move in move_obj.browse(cr, uid, partial_datas[picking.id].keys(), context=context):
 
2768
                # we browse the updated moves (return qty > 0 is checked during data generation)
 
2769
                # data from wizard
 
2770
                data = partial_datas[picking.id][move.id]
 
2771
 
 
2772
                # qty to return
 
2773
                return_qty = data['qty_to_return']
 
2774
                # initial qty is decremented
 
2775
                initial_qty = move.product_qty
 
2776
                initial_qty = max(initial_qty - return_qty, 0)
 
2777
                values = {'product_qty': initial_qty}
 
2778
                
 
2779
                if not initial_qty:
 
2780
                    # if all products are sent back to stock, the move state is cancel - done for now, ideologic question, wahouuu!
 
2781
                    #values.update({'state': 'cancel'})
 
2782
                    values.update({'state': 'done'})
 
2783
                move_obj.write(cr, uid, [move.id], values, context=context)
 
2784
                
 
2785
                # create a back move with the quantity to return to the good location
 
2786
                # the good location is stored in the 'initial_location' field
 
2787
                move_obj.copy(cr, uid, move.id, {'product_qty': return_qty,
 
2788
                                                 'location_dest_id': move.initial_location.id,
 
2789
                                                 'state': 'done'}, context=dict(context, keepLineNumber=True))
 
2790
                
 
2791
                # increase the draft move with the move quantity
 
2792
                draft_move_id = move.backmove_id.id
 
2793
                draft_initial_qty = move_obj.read(cr, uid, [draft_move_id], ['product_qty'], context=context)[0]['product_qty']
 
2794
                draft_initial_qty += return_qty
 
2795
                move_obj.write(cr, uid, [draft_move_id], {'product_qty': draft_initial_qty}, context=context)
 
2796
                
 
2797
            # log the increase action - display the picking ticket view form
 
2798
            # TODO refactoring needed
 
2799
            obj_data = self.pool.get('ir.model.data')
 
2800
            res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_form')[1]
 
2801
            self.log(cr, uid, picking.id, _("Products from Pre-Packing List (%s) have been returned to stock.")%(picking.name,), context={'view_id': res,})
 
2802
            res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
 
2803
            self.log(cr, uid, draft_picking_id, _("The corresponding Draft Picking Ticket (%s) has been updated.")%(picking.previous_step_id.backorder_id.name,), context={'view_id': res,})
 
2804
            # if all moves are done or canceled, the ppl is canceled
 
2805
            cancel_ppl = True
 
2806
            for move in picking.move_lines:
 
2807
                if move.state in ('assigned'):
 
2808
                    cancel_ppl = False
 
2809
            
 
2810
            if cancel_ppl:
 
2811
                # we dont want the back move (done) to be canceled - so we dont use the original cancel workflow state because
 
2812
                # action_cancel() from stock_picking would be called, this would cancel the done stock_moves
 
2813
                # instead we move to the new return_cancel workflow state which simply set the stock_picking state to 'cancel'
 
2814
                # TODO THIS DOESNT WORK - still done state - replace with trigger for now
 
2815
                #wf_service.trg_validate(uid, 'stock.picking', picking.id, 'return_cancel', cr)
 
2816
                wf_service.trg_write(uid, 'stock.picking', picking.id, cr)
 
2817
 
 
2818
        # TODO which behavior
 
2819
        #return {'type': 'ir.actions.act_window_close'}
 
2820
        data_obj = self.pool.get('ir.model.data')
 
2821
        view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
 
2822
        view_id = view_id and view_id[1] or False
 
2823
        context.update({'picking_type': 'picking_ticket'})
 
2824
        return {
 
2825
            'name':_("Picking Ticket"),
 
2826
            'view_mode': 'form,tree',
 
2827
            'view_id': [view_id],
 
2828
            'view_type': 'form',
 
2829
            'res_model': 'stock.picking',
 
2830
            'res_id': draft_picking_id ,
 
2831
            'type': 'ir.actions.act_window',
 
2832
            'target': 'crush',
 
2833
            'context': context,
 
2834
        }
 
2835
    
 
2836
    def action_cancel(self, cr, uid, ids, context=None):
 
2837
        '''
 
2838
        override cancel state action from the workflow
 
2839
        
 
2840
        - depending on the subtype and state of the stock.picking object
 
2841
          the behavior will be different
 
2842
        
 
2843
        Cancel button is active for the picking object:
 
2844
        - subtype: 'picking'
 
2845
        Cancel button is active for the shipment object:
 
2846
        - subtype: 'packing'
 
2847
        
 
2848
        state is not taken into account as picking is canceled before
 
2849
        '''
 
2850
        if context is None:
 
2851
            context = {}
 
2852
        move_obj = self.pool.get('stock.move')
 
2853
        obj_data = self.pool.get('ir.model.data')
 
2854
        
 
2855
        # check the state of the picking
 
2856
        for picking in self.browse(cr, uid, ids, context=context):
 
2857
            # if draft and shipment is in progress, we cannot cancel
 
2858
            if picking.subtype == 'picking' and picking.state in ('draft',):
 
2859
                if self.has_picking_ticket_in_progress(cr, uid, [picking.id], context=context)[picking.id]:
 
2860
                    raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try to cancel again.'))
 
2861
                return super(stock_picking, self).action_cancel(cr, uid, ids, context=context)
 
2862
            # if not draft or qty does not match, the shipping is already in progress
 
2863
            if picking.subtype == 'picking' and picking.state in ('done',):
 
2864
                raise osv.except_osv(_('Warning !'), _('The shipment process is completed and cannot be canceled!'))
 
2865
        
 
2866
        # first call to super method, so if some checks fail won't perform other actions anyway
 
2867
        # call super - picking is canceled
 
2868
        super(stock_picking, self).action_cancel(cr, uid, ids, context=context)
 
2869
        
 
2870
        for picking in self.browse(cr, uid, ids, context=context):
 
2871
                
 
2872
            if picking.subtype == 'picking':
 
2873
                # for each picking
 
2874
                # get the draft picking
 
2875
                draft_picking_id = picking.backorder_id.id
 
2876
                
 
2877
                # for each move from picking ticket - could be split moves
 
2878
                for move in picking.move_lines:
 
2879
                    # find the corresponding move in draft
 
2880
                    draft_move = move.backmove_id
 
2881
                    # increase the draft move with the move quantity
 
2882
                    initial_qty = move_obj.read(cr, uid, [draft_move.id], ['product_qty'], context=context)[0]['product_qty']
 
2883
                    initial_qty += move.product_qty
 
2884
                    move_obj.write(cr, uid, [draft_move.id], {'product_qty': initial_qty}, context=context)
 
2885
                    # log the increase action
 
2886
                    # TODO refactoring needed
 
2887
                    obj_data = self.pool.get('ir.model.data')
 
2888
                    res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
 
2889
                    self.log(cr, uid, draft_picking_id, _("The corresponding Draft Picking Ticket (%s) has been updated.")%(picking.backorder_id.name,), context={'view_id': res,})
 
2890
                    
 
2891
            if picking.subtype == 'packing':
 
2892
                # for each packing we get the draft packing
 
2893
                draft_packing_id = picking.backorder_id.id
 
2894
                
 
2895
                # for each move from the packing
 
2896
                for move in picking.move_lines:
 
2897
                    # corresponding draft move from draft **packing** object
 
2898
                    draft_move_id = move.backmove_packing_id.id
 
2899
                    # check the to_pack of draft move
 
2900
                    # if equal to draft to_pack = move from_pack - 1 (as we always take the pack with the highest number available)
 
2901
                    # we can increase the qty and update draft to_pack
 
2902
                    # otherwise we copy the draft packing move with updated quantity and from/to
 
2903
                    # we always create a new move
 
2904
                    draft_read = move_obj.read(cr, uid, [draft_move_id], ['product_qty', 'to_pack'], context=context)[0]
 
2905
                    draft_to_pack = draft_read['to_pack']
 
2906
                    if draft_to_pack + 1 == move.from_pack and False: # DEACTIVATED
 
2907
                        # updated quantity
 
2908
                        draft_qty = draft_read['product_qty'] + move.product_qty
 
2909
                        # update the draft move
 
2910
                        move_obj.write(cr, uid, [draft_move_id], {'product_qty': draft_qty, 'to_pack': move.to_pack}, context=context)
 
2911
                    else:
 
2912
                        # copy draft move (to be sure not to miss original info) with move qty and from/to
 
2913
                        move_obj.copy(cr, uid, draft_move_id, {'product_qty': move.product_qty,
 
2914
                                                               'from_pack': move.from_pack,
 
2915
                                                               'to_pack': move.to_pack,
 
2916
                                                               'state': 'assigned'}, context=context)
 
2917
        
 
2918
        return True
 
2919
            
 
2920
stock_picking()
 
2921
 
 
2922
 
 
2923
class wizard(osv.osv):
 
2924
    '''
 
2925
    class offering open_wizard method for wizard control
 
2926
    '''
 
2927
    _name = 'wizard'
 
2928
    
 
2929
    def open_wizard(self, cr, uid, ids, name=False, model=False, step='default', type='create', context=None):
 
2930
        '''
 
2931
        WARNING : IDS CORRESPOND TO ***MAIN OBJECT IDS*** (picking for example) take care when calling the method from wizards
 
2932
        return the newly created wizard's id
 
2933
        name, model, step are mandatory only for type 'create'
 
2934
        '''
 
2935
        if context is None:
 
2936
            context = {}
 
2937
        
 
2938
        if type == 'create':
 
2939
            assert name, 'type "create" and no name defined'
 
2940
            assert model, 'type "create" and no model defined'
 
2941
            assert step, 'type "create" and no step defined'
 
2942
            # create the memory object - passing the picking id to it through context
 
2943
            wizard_id = self.pool.get(model).create(
 
2944
                cr, uid, {}, context=dict(context,
 
2945
                                          active_ids=ids,
 
2946
                                          model=model,
 
2947
                                          step=step,
 
2948
                                          back_model=context.get('model', False),
 
2949
                                          back_wizard_ids=context.get('wizard_ids', False),
 
2950
                                          back_wizard_name=context.get('wizard_name', False),
 
2951
                                          back_step=context.get('step', False),
 
2952
                                          wizard_name=name))
 
2953
        
 
2954
        elif type == 'back':
 
2955
            # open the previous wizard
 
2956
            assert context['back_wizard_ids'], 'no back_wizard_ids defined'
 
2957
            wizard_id = context['back_wizard_ids'][0]
 
2958
            assert context['back_wizard_name'], 'no back_wizard_name defined'
 
2959
            name = context['back_wizard_name']
 
2960
            assert context['back_model'], 'no back_model defined'
 
2961
            model = context['back_model']
 
2962
            assert context['back_step'], 'no back_step defined'
 
2963
            step = context['back_step']
 
2964
            
 
2965
        elif type == 'update':
 
2966
            # refresh the same wizard
 
2967
            assert context['wizard_ids'], 'no wizard_ids defined'
 
2968
            wizard_id = context['wizard_ids'][0]
 
2969
            assert context['wizard_name'], 'no wizard_name defined'
 
2970
            name = context['wizard_name']
 
2971
            assert context['model'], 'no model defined'
 
2972
            model = context['model']
 
2973
            assert context['step'], 'no step defined'
 
2974
            step = context['step']
 
2975
            
 
2976
        # call action to wizard view
 
2977
        return {
 
2978
            'name': name,
 
2979
            'view_mode': 'form',
 
2980
            'view_id': False,
 
2981
            'view_type': 'form',
 
2982
            'res_model': model,
 
2983
            'res_id': wizard_id,
 
2984
            'type': 'ir.actions.act_window',
 
2985
            'nodestroy': True,
 
2986
            'target': 'new',
 
2987
            'domain': '[]',
 
2988
            'context': dict(context,
 
2989
                            active_ids=ids,
 
2990
                            wizard_ids=[wizard_id],
 
2991
                            model=model,
 
2992
                            step=step,
 
2993
                            back_model=context.get('model', False),
 
2994
                            back_wizard_ids=context.get('wizard_ids', False),
 
2995
                            back_wizard_name=context.get('wizard_name', False),
 
2996
                            back_step=context.get('step', False),
 
2997
                            wizard_name=name)
 
2998
        }
 
2999
    
 
3000
wizard()
 
3001
 
 
3002
 
 
3003
class product_product(osv.osv):
 
3004
    '''
 
3005
    add a getter for keep cool notion
 
3006
    '''
 
3007
    _inherit = 'product.product'
 
3008
    
 
3009
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
3010
        '''
 
3011
        get functional values
 
3012
        '''
 
3013
        result = {}
 
3014
        for product in self.browse(cr, uid, ids, context=context):
 
3015
            values = {'is_keep_cool': False,
 
3016
                      }
 
3017
            result[product.id] = values
 
3018
            # keep cool
 
3019
            is_keep_cool = bool(product.heat_sensitive_item)# in ('*', '**', '***',)
 
3020
            values['is_keep_cool'] = is_keep_cool
 
3021
                    
 
3022
        return result
 
3023
    
 
3024
    _columns = {'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals',),
 
3025
                'prodlot_ids': fields.one2many('stock.production.lot', 'product_id', string='Batch Numbers',),
 
3026
                }
 
3027
    
 
3028
product_product()
 
3029
 
 
3030
 
 
3031
class stock_move(osv.osv):
 
3032
    '''
 
3033
    stock move
 
3034
    '''
 
3035
    _inherit = 'stock.move'
 
3036
    
 
3037
    def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
 
3038
        '''
 
3039
        facade for product_available function from product (stock)
 
3040
        '''
 
3041
        # get the corresponding product ids
 
3042
        result = {}
 
3043
        for d in self.read(cr, uid, ids, ['product_id'], context):
 
3044
            result[d['id']] = d['product_id'][0]
 
3045
        
 
3046
        # get the virtual stock identified by product ids
 
3047
        virtual = self.pool.get('product.product')._product_available(cr, uid, result.values(), field_names, arg, context)
 
3048
        
 
3049
        # replace product ids by corresponding stock move id
 
3050
        result = dict([id, virtual[result[id]]] for id in result.keys())
 
3051
        return result
 
3052
    
 
3053
    def _vals_get(self, cr, uid, ids, fields, arg, context=None):
 
3054
        '''
 
3055
        get functional values
 
3056
        '''
 
3057
        result = {}
 
3058
        for move in self.browse(cr, uid, ids, context=context):
 
3059
            values = {'qty_per_pack': 0.0,
 
3060
                      'total_amount': 0.0,
 
3061
                      'amount': 0.0,
 
3062
                      'currency_id': False,
 
3063
                      'num_of_packs': 0,
 
3064
                      'is_dangerous_good': False,
 
3065
                      'is_keep_cool': False,
 
3066
                      'is_narcotic': False,
 
3067
                      'sale_order_line_number': 0,
 
3068
                      }
 
3069
            result[move.id] = values
 
3070
            # number of packs with from/to values (integer)
 
3071
            if move.to_pack == 0:
 
3072
                num_of_packs = 0
 
3073
            else:
 
3074
                num_of_packs = move.to_pack - move.from_pack + 1
 
3075
            values['num_of_packs'] = num_of_packs
 
3076
            # quantity per pack
 
3077
            if num_of_packs:
 
3078
                values['qty_per_pack'] = move.product_qty / num_of_packs
 
3079
            else:
 
3080
                values['qty_per_pack'] = 0
 
3081
            # total amount (float)
 
3082
            total_amount = move.sale_line_id and move.sale_line_id.price_unit * move.product_qty or 0.0
 
3083
            values['total_amount'] = total_amount
 
3084
            # amount for one pack
 
3085
            if num_of_packs:
 
3086
                amount = total_amount / num_of_packs
 
3087
            else:
 
3088
                amount = 0
 
3089
            values['amount'] = amount
 
3090
            # currency
 
3091
            values['currency_id'] = move.sale_line_id and move.sale_line_id.currency_id and move.sale_line_id.currency_id.id or False
 
3092
            # dangerous good
 
3093
            values['is_dangerous_good'] = move.product_id and move.product_id.dangerous_goods or False
 
3094
            # keep cool - if heat_sensitive_item is True
 
3095
            values['is_keep_cool'] = bool(move.product_id and move.product_id.heat_sensitive_item or False)
 
3096
            # narcotic
 
3097
            values['is_narcotic'] = move.product_id and move.product_id.narcotic or False
 
3098
            # sale_order_line_number
 
3099
            values['sale_order_line_number'] = move.sale_line_id and move.sale_line_id.line_number or 0
 
3100
                    
 
3101
        return result
 
3102
    
 
3103
    def default_get(self, cr, uid, fields, context=None):
 
3104
        '''
 
3105
        Set default values according to type and subtype
 
3106
        '''
 
3107
        if not context:
 
3108
            context = {}
 
3109
            
 
3110
        res = super(stock_move, self).default_get(cr, uid, fields, context=context)
 
3111
        
 
3112
        if 'warehouse_id' in context and context.get('warehouse_id'):
 
3113
            warehouse_id = context.get('warehouse_id')
 
3114
        else:
 
3115
            warehouse_id = self.pool.get('stock.warehouse').search(cr, uid, [], context=context)[0]
 
3116
        res.update({'location_output_id': self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_output_id.id})
 
3117
        
 
3118
        loc_virtual_ids = self.pool.get('stock.location').search(cr, uid, [('name', '=', 'Virtual Locations')])
 
3119
        loc_virtual_id = len(loc_virtual_ids) > 0 and loc_virtual_ids[0] or False
 
3120
        res.update({'location_virtual_id': loc_virtual_id})
 
3121
        
 
3122
        if 'type' in context and context.get('type', False) == 'out':
 
3123
            loc_stock_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_stock_id.id
 
3124
            res.update({'location_id': loc_stock_id})
 
3125
        
 
3126
        if 'subtype' in context and context.get('subtype', False) == 'picking':
 
3127
            loc_packing_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_packing_id.id
 
3128
            res.update({'location_dest_id': loc_packing_id})
 
3129
        elif 'subtype' in context and context.get('subtype', False) == 'standard':
 
3130
            loc_output_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_output_id.id
 
3131
            res.update({'location_dest_id': loc_output_id})
 
3132
        
 
3133
        return res
 
3134
    
 
3135
    _columns = {'from_pack': fields.integer(string='From p.'),
 
3136
                'to_pack': fields.integer(string='To p.'),
 
3137
                'pack_type': fields.many2one('pack.type', string='Pack Type'),
 
3138
                'length' : fields.float(digits=(16,2), string='Length [cm]'),
 
3139
                'width' : fields.float(digits=(16,2), string='Width [cm]'),
 
3140
                'height' : fields.float(digits=(16,2), string='Height [cm]'),
 
3141
                'weight' : fields.float(digits=(16,2), string='Weight p.p [kg]'),
 
3142
                #'pack_family_id': fields.many2one('pack.family', string='Pack Family'),
 
3143
                'initial_location': fields.many2one('stock.location', string='Initial Picking Location'),
 
3144
                # relation to the corresponding move from draft **picking** ticket object
 
3145
                'backmove_id': fields.many2one('stock.move', string='Corresponding move of previous step'),
 
3146
                # relation to the corresponding move from draft **packing** ticket object
 
3147
                'backmove_packing_id': fields.many2one('stock.move', string='Corresponding move of previous step in draft packing'),
 
3148
                # functions
 
3149
                'virtual_available': fields.function(_product_available, method=True, type='float', string='Virtual Stock', help="Future stock for this product according to the selected locations or all internal if none have been selected. Computed as: Real Stock - Outgoing + Incoming.", multi='qty_available', digits_compute=dp.get_precision('Product UoM')),
 
3150
                'qty_per_pack': fields.function(_vals_get, method=True, type='float', string='Qty p.p', multi='get_vals',),
 
3151
                'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals',),
 
3152
                'amount': fields.function(_vals_get, method=True, type='float', string='Pack Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals',),
 
3153
                'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals_X',), # old_multi get_vals
 
3154
                'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
 
3155
                'is_dangerous_good': fields.function(_vals_get, method=True, type='boolean', string='Dangerous Good', multi='get_vals',),
 
3156
                'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals',),
 
3157
                'is_narcotic': fields.function(_vals_get, method=True, type='boolean', string='Narcotic', multi='get_vals',),
 
3158
                'sale_order_line_number': fields.function(_vals_get, method=True, type='integer', string='Sale Order Line Number', multi='get_vals_X',), # old_multi get_vals
 
3159
                # Fields used for domain
 
3160
                'location_virtual_id': fields.many2one('stock.location', string='Virtual location'),
 
3161
                'location_output_id': fields.many2one('stock.location', string='Output location'),
 
3162
                'invoice_line_id': fields.many2one('account.invoice.line', string='Invoice line'),
 
3163
                }
 
3164
 
 
3165
    def action_cancel(self, cr, uid, ids, context=None):
 
3166
        '''
 
3167
            Confirm or check the procurement order associated to the stock move
 
3168
        '''
 
3169
        res = super(stock_move, self).action_cancel(cr, uid, ids, context=context)
 
3170
        
 
3171
        wf_service = netsvc.LocalService("workflow")
 
3172
 
 
3173
        proc_obj = self.pool.get('procurement.order')
 
3174
        proc_ids = proc_obj.search(cr, uid, [('move_id', 'in', ids)], context=context)
 
3175
        for proc in proc_obj.browse(cr, uid, proc_ids, context=context):
 
3176
            if proc.state == 'draft':
 
3177
                wf_service.trg_validate(uid, 'procurement.order', proc.id, 'button_confirm', cr)
 
3178
            else:
 
3179
                wf_service.trg_validate(uid, 'procurement.order', proc.id, 'button_check', cr)
 
3180
        
 
3181
        return res
 
3182
 
 
3183
stock_move()
 
3184
 
 
3185
 
 
3186
class sale_order(osv.osv):
 
3187
    '''
 
3188
    re-override to modify behavior for outgoing workflow
 
3189
    '''
 
3190
    _inherit = 'sale.order'
 
3191
    _name = 'sale.order'
 
3192
    
 
3193
    def _hook_ship_create_execute_specific_code_01(self, cr, uid, ids, context=None, *args, **kwargs):
 
3194
        '''
 
3195
        Please copy this to your module's method also.
 
3196
        This hook belongs to the action_ship_create method from sale>sale.py
 
3197
        
 
3198
        - allow to execute specific code at position 01
 
3199
        '''
 
3200
        super(sale_order, self)._hook_ship_create_execute_specific_code_01(cr, uid, ids, context=context, *args, **kwargs)
 
3201
        # objects
 
3202
        pick_obj = self.pool.get('stock.picking')
 
3203
        
 
3204
        wf_service = netsvc.LocalService("workflow")
 
3205
        proc_id = kwargs['proc_id']
 
3206
        order = kwargs['order']
 
3207
        if order.procurement_request :
 
3208
            proc = self.pool.get('procurement.order').browse(cr, uid, [proc_id], context=context)
 
3209
            pick_id = proc and proc[0] and proc[0].move_id and proc[0].move_id.picking_id and proc[0].move_id.picking_id.id or False
 
3210
            if pick_id:
 
3211
                wf_service.trg_validate(uid, 'stock.picking', [pick_id], 'button_confirm', cr)
 
3212
 
 
3213
                # We also do a first 'check availability': cancel then check
 
3214
                pick_obj.cancel_assign(cr, uid, [pick_id], context)
 
3215
                pick_obj.action_assign(cr, uid, [pick_id], context)
 
3216
        
 
3217
        return True
 
3218
    
 
3219
    def _hook_ship_create_execute_specific_code_02(self, cr, uid, ids, context=None, *args, **kwargs):
 
3220
        '''
 
3221
        Please copy this to your module's method also.
 
3222
        This hook belongs to the action_ship_create method from sale>sale.py
 
3223
        
 
3224
        - allow to execute specific code at position 02
 
3225
        '''
 
3226
        # Some verifications
 
3227
        if context is None:
 
3228
            context = {}
 
3229
        if isinstance(ids, (int, long)):
 
3230
            ids = [ids]
 
3231
        
 
3232
        # objects
 
3233
        pick_obj = self.pool.get('stock.picking')
 
3234
        move_obj = self.pool.get('stock.move')
 
3235
        order = kwargs['order']
 
3236
        move_id = kwargs['move_id']
 
3237
        pick_id = move_obj.browse(cr, uid, [move_id], context=context)[0].picking_id.id
 
3238
        
 
3239
        if order.procurement_request:
 
3240
            move_obj.action_confirm(cr, uid, [move_id], context=context)
 
3241
            # we Validate the picking "Confirms picking directly from draft state."
 
3242
            pick_obj.draft_force_assign(cr, uid , [pick_id], context)
 
3243
            # We also do a first 'check availability': cancel then check
 
3244
            pick_obj.cancel_assign(cr, uid, [pick_id], context)
 
3245
            pick_obj.action_assign(cr, uid, [pick_id], context)
 
3246
            
 
3247
        return super(sale_order, self)._hook_ship_create_execute_specific_code_02(cr, uid, ids, context, *args, **kwargs)
 
3248
    
 
3249
    def _hook_ship_create_stock_move(self, cr, uid, ids, context=None, *args, **kwargs):
 
3250
        '''
 
3251
        Please copy this to your module's method also.
 
3252
        This hook belongs to the action_ship_create method from sale>sale.py
 
3253
        
 
3254
        - allow to modify the data for stock move creation
 
3255
        '''
 
3256
        move_data = super(sale_order, self)._hook_ship_create_stock_move(cr, uid, ids, context=context, *args, **kwargs)
 
3257
        order = kwargs['order']
 
3258
        # For IR
 
3259
        if self.read(cr, uid, ids, ['procurement_request'], context=context)[0]['procurement_request']\
 
3260
        and self.read(cr, uid, ids, ['location_requestor_id'], context=context)[0]['location_requestor_id']:
 
3261
            move_data['type'] = 'internal'
 
3262
            move_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
 
3263
            move_data['location_dest_id'] = self.read(cr, uid, ids, ['location_requestor_id'], context=context)[0]['location_requestor_id'][0]
 
3264
        else:
 
3265
            # first go to packing location (PICK/PACK/SHIP) or output location (Simple OUT)
 
3266
            # according to the configuration
 
3267
            # first go to packing location
 
3268
            setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
 
3269
            if setup.delivery_process == 'simple':
 
3270
                move_data['location_dest_id'] = order.shop_id.warehouse_id.lot_output_id.id
 
3271
            else:
 
3272
                move_data['location_dest_id'] = order.shop_id.warehouse_id.lot_packing_id.id
 
3273
                
 
3274
            if self.pool.get('product.product').browse(cr, uid, move_data['product_id']).type == 'service_recep':
 
3275
                move_data['location_id'] = self.pool.get('stock.location').get_cross_docking_location(cr, uid)
 
3276
            
 
3277
            if 'sale_line_id' in move_data and move_data['sale_line_id']:
 
3278
                sale_line = self.pool.get('sale.order.line').browse(cr, uid, move_data['sale_line_id'], context=context)
 
3279
                if sale_line.type == 'make_to_order':
 
3280
                    move_data['location_id'] = self.pool.get('stock.location').get_cross_docking_location(cr, uid)
 
3281
                    move_data['move_cross_docking_ok'] = True
 
3282
                    # Update the stock.picking
 
3283
                    self.pool.get('stock.picking').write(cr, uid, move_data['picking_id'], {'cross_docking_ok': True}, context=context)
 
3284
 
 
3285
        move_data['state'] = 'confirmed'
 
3286
        return move_data
 
3287
    
 
3288
    def _hook_ship_create_stock_picking(self, cr, uid, ids, context=None, *args, **kwargs):
 
3289
        '''
 
3290
        Please copy this to your module's method also.
 
3291
        This hook belongs to the action_ship_create method from sale>sale.py
 
3292
        
 
3293
        - allow to modify the data for stock picking creation
 
3294
        '''
 
3295
        setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
 
3296
        
 
3297
        picking_data = super(sale_order, self)._hook_ship_create_stock_picking(cr, uid, ids, context=context, *args, **kwargs)
 
3298
        order = kwargs['order']
 
3299
        
 
3300
        picking_data['state'] = 'draft'
 
3301
        if setup.delivery_process == 'simple':
 
3302
            picking_data['subtype'] = 'standard'
 
3303
            # use the name according to picking ticket sequence
 
3304
            pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
 
3305
        else:
 
3306
            picking_data['subtype'] = 'picking'
 
3307
            # use the name according to picking ticket sequence
 
3308
            pick_name = self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket')
 
3309
            
 
3310
        # For IR
 
3311
        if self.read(cr, uid, ids, ['procurement_request'], context=context):
 
3312
            procurement_request = self.read(cr, uid, ids, ['procurement_request'], context=context)[0]['procurement_request']
 
3313
            if procurement_request:
 
3314
                picking_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
 
3315
                picking_data['type'] = 'internal'
 
3316
                picking_data['subtype'] = 'standard'
 
3317
                picking_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
 
3318
                pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.internal')
 
3319
            
 
3320
        picking_data['name'] = pick_name        
 
3321
        picking_data['flow_type'] = 'full'
 
3322
        picking_data['backorder_id'] = False
 
3323
        picking_data['warehouse_id'] = order.shop_id.warehouse_id.id
 
3324
        
 
3325
        return picking_data
 
3326
    
 
3327
    def _hook_ship_create_execute_picking_workflow(self, cr, uid, ids, context=None, *args, **kwargs):
 
3328
        '''
 
3329
        Please copy this to your module's method also.
 
3330
        This hook belongs to the action_ship_create method from sale>sale.py
 
3331
        
 
3332
        - allow to avoid the stock picking workflow execution
 
3333
        - trigger the logging message for the created picking, as it stays in draft state and no call to action_confirm is performed
 
3334
          for the moment within the msf_outgoing logic
 
3335
        '''
 
3336
        setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
 
3337
        cond = super(sale_order, self)._hook_ship_create_execute_picking_workflow(cr, uid, ids, context=context, *args, **kwargs)
 
3338
 
 
3339
        # On Simple OUT configuration, the system should confirm the OUT and launch a first check availability
 
3340
        if setup.delivery_process != 'simple':
 
3341
            cond = cond and False
 
3342
        
 
3343
        # diplay creation message for draft picking ticket
 
3344
        picking_id = kwargs['picking_id']
 
3345
        picking_obj = self.pool.get('stock.picking')
 
3346
        if picking_id:
 
3347
            picking_obj.log_picking(cr, uid, [picking_id], context=context)
 
3348
            # Launch a first check availability
 
3349
            self.pool.get('stock.picking').action_assign(cr, uid, [picking_id], context=context)
 
3350
        
 
3351
        return cond
 
3352
 
 
3353
sale_order()
 
3354
 
 
3355
 
 
3356
class procurement_order(osv.osv):
 
3357
    '''
 
3358
    procurement order workflow
 
3359
    '''
 
3360
    _inherit = 'procurement.order'
 
3361
    
 
3362
    def _hook_check_mts_on_message(self, cr, uid, context=None, *args, **kwargs):
 
3363
        '''
 
3364
        Please copy this to your module's method also.
 
3365
        This hook belongs to the _check_make_to_stock_product method from procurement>procurement.py>procurement.order
 
3366
        
 
3367
        - allow to modify the message written back to procurement order
 
3368
        '''
 
3369
        message = super(procurement_order, self)._hook_check_mts_on_message(cr, uid, context=context, *args, **kwargs)
 
3370
        procurement = kwargs['procurement']
 
3371
        if procurement.move_id.picking_id.state == 'draft' and procurement.move_id.picking_id.subtype == 'picking':
 
3372
            message = _("Shipment Process in Progress.")
 
3373
        return message
 
3374
    
 
3375
procurement_order()
 
3376