1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 MSF, TeMPO Consulting
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Affero General Public License as
9
# published by the Free Software Foundation, either version 3 of the
10
# License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Affero General Public License for more details.
17
# You should have received a copy of the GNU Affero General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
22
from osv import osv, fields
23
from tools.translate import _
25
from datetime import datetime, timedelta, date
27
from dateutil.relativedelta import relativedelta
28
import decimal_precision as dp
34
class stock_warehouse(osv.osv):
36
add new packing, dispatch and distribution locations for input
38
_inherit = "stock.warehouse"
39
_name = "stock.warehouse"
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')]),
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,
54
class pack_type(osv.osv):
56
pack type corresponding to a type of pack (name, length, width, height)
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]'),
69
class shipment(osv.osv):
71
a shipment presents the data from grouped stock moves in a 'sequence' way
74
_description = 'represents a group of pack families'
76
def copy(self, cr, uid, id, default=None, context=None):
80
raise osv.except_osv(_('Error !'), _('Shipment copy is forbidden.'))
82
def copy_data(self, cr, uid, id, default=None, context=None):
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)
96
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
98
multi function for global shipment values
100
pf_memory_obj = self.pool.get('pack.family.memory')
101
picking_obj = self.pool.get('stock.picking')
104
for shipment in self.browse(cr, uid, ids, context=context):
105
values = {'total_amount': 0.0,
106
'currency_id': False,
111
'backshipment_id': False,
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
119
first_shipment_packing_id = None
120
backshipment_id = None
122
delivery_validated = None
123
# browse the corresponding packings
124
for packing in picking_obj.browse(cr, uid, packing_ids, context=context):
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
129
state = packing.state
131
# all corresponding shipment must be dev validated or not
132
if packing.delivered:
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)
138
delivery_validated = packing.delivered
140
# first_shipment_packing_id check - no check for the same reason
141
first_shipment_packing_id = packing.first_shipment_packing_id.id
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
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
155
elif state == 'done':
156
if delivery_validated:
157
# special state corresponding to delivery validated
160
values['state'] = state
161
values['backshipment_id'] = backshipment_id
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',) or memory_family.state not in ('done',) :
167
num_of_packs = memory_family.num_of_packs
168
values['num_of_packs'] += int(num_of_packs)
170
total_weight = memory_family.total_weight
171
values['total_weight'] += int(total_weight)
173
total_volume = memory_family.total_volume
174
values['total_volume'] += float(total_volume)
176
total_amount = memory_family.total_amount
177
values['total_amount'] += total_amount
179
currency_id = memory_family.currency_id and memory_family.currency_id.id or False
180
values['currency_id'] = currency_id
184
def _get_shipment_ids(self, cr, uid, ids, context=None):
186
ids represents the ids of stock.picking objects for which state has changed
188
return the list of ids of shipment object which need to get their state field updated
190
pack_obj = self.pool.get('stock.picking')
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)
197
def _packs_search(self, cr, uid, obj, name, args, context=None):
199
Searches Ids of shipment
204
shipments = self.pool.get('shipment').search(cr, uid, [], context=context)
207
for shipment in self.browse(cr, uid, shipments, context=context):
208
result[shipment.id] = shipment.num_of_packs
209
# construct the request
214
ids = [('id', 'in', [x for x in result.keys() if eval("%s %s %s"%(result[x], op, args[0][2]))])]
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),
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),
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),
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),
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'),
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'),
276
_defaults = {'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),}
281
def create_shipment(self, cr, uid, ids, context=None):
283
open the wizard to create (partial) shipment
285
# we need the context for the wizard switch
288
context['group_by'] = False
290
wiz_obj = self.pool.get('wizard')
293
name = _("Create Shipment")
294
model = 'shipment.wizard'
296
# open the selected wizard
297
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
299
def do_create_shipment(self, cr, uid, ids, context=None):
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
309
assert context, 'no context, method call is wrong'
310
assert 'partial_datas_shipment' in context, 'partial_datas_shipment no defined in context'
312
pick_obj = self.pool.get('stock.picking')
313
move_obj = self.pool.get('stock.move')
314
shipment_obj = self.pool.get('shipment')
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'
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
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,))
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])
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)
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
362
'name':_("Shipment"),
363
'view_mode': 'form,tree',
364
'view_id': [view_id],
366
'res_model': 'shipment',
367
'res_id': shipment_id,
368
'type': 'ir.actions.act_window',
372
def return_packs(self, cr, uid, ids, context=None):
374
open the wizard to return packs from draft shipment
376
# we need the context for the wizard switch
380
wiz_obj = self.pool.get('wizard')
383
name = _("Return Packs")
384
model = 'shipment.wizard'
386
# open the selected wizard
387
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
389
def do_return_packs(self, cr, uid, ids, context=None):
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
399
assert context, 'no context, method call is wrong'
400
assert 'partial_datas' in context, 'partial_datas no defined in context'
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')
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'
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
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
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
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
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
451
initial_qty = move.product_qty
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,}
460
move_obj.write(cr, uid, [move.id], values, context=context)
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=context)
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)
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
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,))
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,})
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)
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
494
'name':_("Picking Ticket"),
495
'view_mode': 'form,tree',
496
'view_id': [view_id],
498
'res_model': 'stock.picking',
499
'res_id': draft_picking_id ,
500
'type': 'ir.actions.act_window',
504
def return_packs_from_shipment(self, cr, uid, ids, context=None):
506
open the wizard to return packs from draft shipment
508
# we need the context for the wizard switch
512
wiz_obj = self.pool.get('wizard')
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)
521
def compute_sequences(self, cr, uid, ids, context=None, *args, **kwargs):
523
compute corresponding sequences
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
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
541
# find the corresponding tuple in the stay list
542
for i in range(len(stay)):
543
# the tuple are ordered
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
553
# to+1-seq[1] in stay
554
stay.append((return_to+1, seq[1]))
557
elif return_to == seq[1]:
558
# do not start at beginning, but same end
559
stay.append((seq[0], return_from-1))
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]))
568
# old one is always removed
571
# return both values - return order is important
572
return stay, back_to_draft
574
def do_return_packs_from_shipment(self, cr, uid, ids, context=None):
576
return the packs to the corresponding draft packing object
578
for each corresponding draft packing
582
assert context, 'no context, method call is wrong'
583
assert 'partial_datas' in context, 'partial_datas no defined in context'
585
pick_obj = self.pool.get('stock.picking')
586
move_obj = self.pool.get('stock.move')
587
wf_service = netsvc.LocalService("workflow")
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'
595
for shipment_id in partial_datas:
597
for packing in pick_obj.browse(cr, uid, partial_datas[shipment_id].keys(), context=context):
598
# corresponding draft packing -> backorder
599
draft_packing_id = packing.backorder_id.id
600
# corresponding draft shipment (all packing for a shipment belong to the same draft_shipment)
601
draft_shipment_id = packing.backorder_id.shipment_id.id
603
for from_pack in partial_datas[shipment_id][packing.id]:
604
for to_pack in partial_datas[shipment_id][packing.id][from_pack]:
605
# partial datas for one sequence of one packing
606
# could have multiple data multiple products in the same pack family
607
datas = partial_datas[shipment_id][packing.id][from_pack][to_pack]
608
# the corresponding moves
609
move_ids = move_obj.search(cr, uid, [('picking_id', '=', packing.id),
610
('from_pack', '=', from_pack),
611
('to_pack', '=', to_pack)], context=context)
613
# compute the sequences to stay/to return to draft packing
614
stay, back_to_draft = self.compute_sequences(cr, uid, ids, context=context,
619
# we have the information concerning movements to update the packing and the draft packing
621
# update the packing object, we update the existing move
622
# if needed new moves are created
624
for move in move_obj.browse(cr, uid, move_ids, context=context):
626
updated[move.id] = {'initial': move.product_qty, 'partial_qty': 0}
627
# loop through stay sequences
629
# corresponding number of packs
630
selected_number = seq[1] - seq[0] + 1
632
new_qty = selected_number * move.qty_per_pack
633
# for both cases, we update the from/to and compute the corresponding quantity
634
# if the move has been updated already, we copy/update
635
values = {'from_pack': seq[0],
637
'product_qty': new_qty,
640
# the original move is never modified, but canceled
641
updated[move.id]['partial_qty'] += new_qty
642
new_move_id = move_obj.copy(cr, uid, move.id, values, context=context)
645
# if 'partial_qty' not in updated[move.id]:
646
# updated[move.id]['partial_qty'] = 0
648
# loop through back_to_draft sequences
649
for seq in back_to_draft:
650
# for each sequence we add the corresponding stock move to draft packing
651
# corresponding number of packs
652
selected_number = seq[1] - seq[0] + 1
654
new_qty = selected_number * move.qty_per_pack
656
location_dispatch = move.picking_id.warehouse_id.lot_dispatch_id.id
657
location_distrib = move.picking_id.warehouse_id.lot_distribution_id.id
658
values = {'from_pack': seq[0],
660
'product_qty': new_qty,
661
'location_id': location_distrib,
662
'location_dest_id': location_dispatch,
665
# create a back move in the packing object
666
# distribution -> dispatch
667
new_back_move_id = move_obj.copy(cr, uid, move.id, values, context=context)
668
updated[move.id]['partial_qty'] += new_qty
670
# create the draft move
671
# dispatch -> distribution
672
# picking_id = draft_picking
673
values.update(location_id=location_dispatch,
674
location_dest_id=location_distrib,
675
picking_id=draft_packing_id,
677
new_draft_move_id = move_obj.copy(cr, uid, move.id, values, context=context)
679
# quantities are right - stay + return qty = original qty
680
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)
681
# if packs are returned corresponding move is canceled
682
# cancel move or 0 qty + done ?
683
#move_obj.action_cancel(cr, uid, [move.id], context=context)
684
move_obj.write(cr, uid, [move.id], {'product_qty': 0.0, 'state': 'done', 'from_pack': 0, 'to_pack': 0,}, context=context)
686
# log corresponding action
687
shipment_name = self.read(cr, uid, shipment_id, ['name'], context=context)['name']
688
self.log(cr, uid, shipment_id, _("Packs from the shipped Shipment (%s) have been returned to dispatch location.")%(shipment_name,))
689
self.log(cr, uid, draft_shipment_id, _("The corresponding Draft Shipment (%s) has been updated.")%(packing.backorder_id.shipment_id.name,))
691
# call complete_finished on the shipment object
692
# if everything is allright (all draft packing are finished) the shipment is done also
693
self.complete_finished(cr, uid, partial_datas.keys(), context=context)
695
# TODO which behavior
696
#return {'type': 'ir.actions.act_window_close'}
697
data_obj = self.pool.get('ir.model.data')
698
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
699
view_id = view_id and view_id[1] or False
701
'name':_("Shipment"),
702
'view_mode': 'form,tree',
703
'view_id': [view_id],
705
'res_model': 'shipment',
706
'res_id': draft_shipment_id,
707
'type': 'ir.actions.act_window',
711
return {'type': 'ir.actions.act_window_close'}
713
def action_cancel(self, cr, uid, ids, context=None):
715
cancel the shipment which is not yet shipped (packed state)
717
- for each related packing object
718
- trigger the cancel workflow signal
719
logic is performed in the action_cancel method of stock.picking
721
pick_obj = self.pool.get('stock.picking')
722
wf_service = netsvc.LocalService("workflow")
724
for shipment in self.browse(cr, uid, ids, context=context):
725
# shipment state should be 'packed'
726
assert shipment.state == 'packed', 'cannot ship a shipment which is not in correct state - packed - %s'%shipment.state
728
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id)], context=context)
729
# call cancel workflow on corresponding packing objects
730
for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
731
# we cancel each picking object - action_cancel is overriden at stock_picking level for stock_picking of subtype == 'packing'
732
wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_cancel', cr)
733
# log corresponding action
734
self.log(cr, uid, shipment.id, _("The Shipment (%s) has been canceled.")%(shipment.name,))
735
self.log(cr, uid, shipment.backshipment_id.id, _("The corresponding Draft Shipment (%s) has been updated.")%(shipment.backshipment_id.name,))
739
def ship(self, cr, uid, ids, context=None):
741
we ship the created shipment, the state of the shipment is changed, we do not use any wizard
742
- state of the shipment is updated to 'shipped'
744
- modify locations of moves for the new packing
745
- trigger the workflow button_confirm for the new packing
746
- trigger the workflow to terminate the initial packing
747
- update the draft_picking_id fields of pack_families
748
- update the shipment_date of the corresponding sale_order if not set yet
750
pick_obj = self.pool.get('stock.picking')
751
pf_obj = self.pool.get('pack.family')
752
so_obj = self.pool.get('sale.order')
754
date_tools = self.pool.get('date.tools')
755
db_datetime_format = date_tools.get_db_datetime_format(cr, uid, context=context)
757
for shipment in self.browse(cr, uid, ids, context=context):
758
# shipment state should be 'packed'
759
assert shipment.state == 'packed', 'cannot ship a shipment which is not in correct state - packed - %s'%shipment.state
760
# the state does not need to be updated - function
761
# update actual ship date (shipment_actual_date) to today + time
762
today = time.strftime(db_datetime_format)
763
shipment.write({'shipment_actual_date': today,})
764
# corresponding packing objects
765
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id)], context=context)
767
for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
768
assert packing.subtype == 'packing'
769
# update the packing object for the same reason
770
# - an integrity check at _get_vals level of shipment states that all packing linked to a shipment must have the same state
771
# we therefore modify it before the copy, otherwise new (assigned) and old (done) are linked to the same shipment
772
# -> integrity check has been removed
773
pick_obj.write(cr, uid, [packing.id], {'shipment_id': False,}, context=context)
775
new_packing_id = pick_obj.copy(cr, uid, packing.id, {'name': packing.name,
776
'first_shipment_packing_id': packing.id,
777
'shipment_id': shipment.id,}, context=dict(context, keep_prodlot=True, allow_copy=True,))
778
pick_obj.write(cr, uid, [new_packing_id], {'origin': packing.origin}, context=context)
779
new_packing = pick_obj.browse(cr, uid, new_packing_id, context=context)
780
# update the shipment_date of the corresponding sale order if the date is not set yet - with current date
781
if new_packing.sale_id and not new_packing.sale_id.shipment_date:
782
# get the date format
783
date_tools = self.pool.get('date.tools')
784
date_format = date_tools.get_date_format(cr, uid, context=context)
785
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
786
today = time.strftime(date_format)
787
today_db = time.strftime(db_date_format)
788
so_obj.write(cr, uid, [new_packing.sale_id.id], {'shipment_date': today_db,}, context=context)
789
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))
791
# update locations of stock moves
792
for move in new_packing.move_lines:
793
move.write({'location_id': new_packing.warehouse_id.lot_distribution_id.id,
794
'location_dest_id': new_packing.warehouse_id.lot_output_id.id}, context=context)
796
wf_service = netsvc.LocalService("workflow")
797
wf_service.trg_validate(uid, 'stock.picking', new_packing_id, 'button_confirm', cr)
798
# simulate check assign button, as stock move must be available
799
pick_obj.force_assign(cr, uid, [new_packing_id])
800
# trigger standard workflow
801
pick_obj.action_move(cr, uid, [packing.id])
802
wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_done', cr)
804
# log the ship action
805
self.log(cr, uid, shipment.id, _('The Shipment %s has been shipped.')%(shipment.name,))
807
# TODO which behavior
810
def complete_finished(self, cr, uid, ids, context=None):
812
- check all draft packing corresponding to this shipment
813
- check the stock moves (qty and from/to)
814
- check all corresponding packing are done or canceled (no ongoing shipment)
815
- if all packings are ok, the draft is validated
816
- if all draft packing are ok, the shipment state is done
818
pick_obj = self.pool.get('stock.picking')
819
wf_service = netsvc.LocalService("workflow")
821
for shipment_base in self.browse(cr, uid, ids, context=context):
822
# the shipment which will be treated
823
shipment = shipment_base
825
if shipment.state not in ('draft',):
826
# it's not a draft shipment, check all corresponding packing, trg.write them
827
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
828
for packing_id in packing_ids:
829
wf_service.trg_write(uid, 'stock.picking', packing_id, cr)
831
# this shipment is possibly finished, we now check the corresponding draft shipment
832
# this will possibly validate the draft shipment, if everything is finished and corresponding draft picking
833
shipment = shipment.backshipment_id
835
# draft packing for this shipment - some draft packing can already be done for this shipment, so we filter according to state
836
draft_packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '=', 'draft'),], context=context)
837
for draft_packing in pick_obj.browse(cr, uid, draft_packing_ids, context=context):
838
assert draft_packing.subtype == 'packing', 'draft packing which is not packing subtype - %s'%draft_packing.subtype
839
assert draft_packing.state == 'draft', 'draft packing which is not draft state - %s'%draft_packing.state
840
# we check if the corresponding draft packing can be moved to done.
841
# if all packing with backorder_id equal to draft are done or canceled
842
# and the quantity for each stock move (state != done) of the draft packing is equal to zero
844
# we first check the stock moves quantities of the draft packing
845
# we can have done moves when some packs are returned
847
for move in draft_packing.move_lines:
848
if move.state not in ('done',):
851
elif move.from_pack or move.to_pack:
852
# qty = 0, from/to pack should have been set to zero
853
assert False, 'stock moves with 0 quantity but part of pack family sequence'
855
# check if ongoing packing are present, if present, we do not validate the draft one, the shipping is not finished
857
linked_packing_ids = pick_obj.search(cr, uid, [('backorder_id', '=', draft_packing.id),
858
('state', 'not in', ['done', 'cancel'])], context=context)
859
if linked_packing_ids:
863
# trigger the workflow for draft_picking
864
# confirm the new picking ticket
865
wf_service.trg_validate(uid, 'stock.picking', draft_packing.id, 'button_confirm', cr)
866
# we force availability
867
pick_obj.force_assign(cr, uid, [draft_packing.id])
869
pick_obj.action_move(cr, uid, [draft_packing.id])
870
wf_service.trg_validate(uid, 'stock.picking', draft_packing.id, 'button_done', cr)
871
# ask for draft picking validation, depending on picking completion
872
# if picking ticket is not completed, the validation will not complete
873
draft_packing.previous_step_id.previous_step_id.backorder_id.validate(context=context)
875
# all draft packing are validated (done state) - the state of shipment is automatically updated -> function
878
def shipment_create_invoice(self, cr, uid, ids, context=None):
880
Create invoices for validated shipment
882
invoice_obj = self.pool.get('account.invoice')
883
line_obj = self.pool.get('account.invoice.line')
884
partner_obj = self.pool.get('res.partner')
885
distrib_obj = self.pool.get('analytic.distribution')
886
sale_line_obj = self.pool.get('sale.order.line')
887
sale_obj = self.pool.get('sale.order')
888
company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
893
if isinstance(ids, (int, long)):
896
for shipment in self.browse(cr, uid, ids, context=context):
898
for pack in shipment.pack_family_memory_ids:
899
for move in pack.move_lines:
900
if move.state != 'cancel' and (not move.sale_line_id or move.sale_line_id.order_id.order_policy == 'picking'):
906
payment_term_id = False
907
partner = shipment.partner_id2
909
raise osv.except_osv(_('Error, no partner !'),
910
_('Please put a partner on the shipment if you want to generate invoice.'))
912
inv_type = 'out_invoice'
914
if inv_type in ('out_invoice', 'out_refund'):
915
account_id = partner.property_account_receivable.id
916
payment_term_id = partner.property_payment_term and partner.property_payment_term.id or False
918
account_id = partner.property_account_payable.id
920
addresses = partner_obj.address_get(cr, uid, [partner.id], ['contact', 'invoice'])
921
today = time.strftime('%Y-%m-%d',time.localtime())
924
'name': shipment.name,
925
'origin': shipment.name or '',
927
'account_id': account_id,
928
'partner_id': partner.id,
929
'address_invoice_id': addresses['invoice'],
930
'address_contact_id': addresses['contact'],
931
'payment_term': payment_term_id,
932
'fiscal_position': partner.property_account_position.id,
933
'date_invoice': context.get('date_inv',False) or today,
937
cur_id = shipment.pack_family_memory_ids[0].currency_id.id
939
invoice_vals['currency_id'] = cur_id
941
journal_type = 'sale'
942
# Disturb journal for invoice only on intermission partner type
943
if shipment.partner_id2.partner_type == 'intermission':
944
if not company.intermission_default_counterpart or not company.intermission_default_counterpart.id:
945
raise osv.except_osv(_('Error'), _('Please configure a default intermission account in Company configuration.'))
946
invoice_vals['is_intermission'] = True
947
invoice_vals['account_id'] = company.intermission_default_counterpart.id
948
journal_type = 'intermission'
949
journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', journal_type),
950
('is_current_instance', '=', True)])
952
raise osv.except_osv(_('Warning'), _('No %s journal found!') % (journal_type,))
953
invoice_vals['journal_id'] = journal_ids[0]
955
invoice_id = invoice_obj.create(cr, uid, invoice_vals,
958
# Change currency for the intermission invoice
959
if shipment.partner_id2.partner_type == 'intermission':
960
company_currency = company.currency_id and company.currency_id.id or False
961
if not company_currency:
962
raise osv.except_osv(_('Warning'), _('No company currency found!'))
963
wiz_account_change = self.pool.get('account.change.currency').create(cr, uid, {'currency_id': company_currency})
964
self.pool.get('account.change.currency').change_currency(cr, uid, [wiz_account_change], context={'active_id': invoice_id})
966
# Link the invoice to the shipment
967
self.write(cr, uid, [shipment.id], {'invoice_id': invoice_id}, context=context)
969
# For each stock moves, create an invoice line
970
for pack in shipment.pack_family_memory_ids:
971
for move in pack.move_lines:
972
if move.state == 'cancel':
975
if move.sale_line_id and move.sale_line_id.order_id.order_policy != 'picking':
978
origin = move.picking_id.name or ''
979
if move.picking_id.origin:
980
origin += ':' + move.picking_id.origin
982
if inv_type in ('out_invoice', 'out_refund'):
983
account_id = move.product_id.product_tmpl_id.\
984
property_account_income.id
986
account_id = move.product_id.categ_id.\
987
property_account_income_categ.id
989
account_id = move.product_id.product_tmpl_id.\
990
property_account_expense.id
992
account_id = move.product_id.categ_id.\
993
property_account_expense_categ.id
995
# Compute unit price from FO line if the move is linked to
996
price_unit = move.product_id.list_price
997
if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
998
uom_id = move.product_id.uom_id.id
999
uos_id = move.product_id.uos_id and move.product_id.uos_id.id or False
1000
price = move.sale_line_id.price_unit
1001
coeff = move.product_id.uos_coeff
1002
if uom_id != uos_id and coeff != 0:
1003
price_unit = price / coeff
1005
price_unit = move.sale_line_id.price_unit
1007
# Get discount from FO line
1009
if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
1010
discount = move.sale_line_id.discount
1012
# Get taxes from FO line
1013
taxes = move.product_id.taxes_id
1014
if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
1015
taxes = [x.id for x in move.sale_line_id.tax_id]
1017
if shipment.partner_id2:
1018
tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, shipment.partner_id2.property_account_position, taxes)
1020
tax_ids = map(lambda x: x.id, taxes)
1023
if move.sale_line_id:
1024
sol_ana_dist_id = move.sale_line_id.analytic_distribution_id or move.sale_line_id.order_id.analytic_distribution_id
1026
distrib_id = distrib_obj.copy(cr, uid, sol_ana_dist_id.id, context=context)
1028
#set UoS if it's a sale and the picking doesn't have one
1029
uos_id = move.product_uos and move.product_uos.id or False
1030
if not uos_id and inv_type in ('out_invoice', 'out_refund'):
1031
uos_id = move.product_uom.id
1032
account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
1034
line_id = line_obj.create(cr, uid, {'name': move.name,
1036
'invoice_id': invoice_id,
1038
'product_id': move.product_id.id,
1039
'account_id': account_id,
1040
'price_unit': price_unit,
1041
'discount': discount,
1042
'quantity': move.product_qty or move.product_uos_qty,
1043
'invoice_line_tax_id': [(6, 0, tax_ids)],
1044
'analytic_distribution_id': distrib_id,
1047
self.pool.get('shipment').write(cr, uid, [shipment.id], {'invoice_id': invoice_id}, context=context)
1048
if move.sale_line_id:
1049
sale_obj.write(cr, uid, [move.sale_line_id.order_id.id], {'invoice_ids': [(4, invoice_id)],})
1050
sale_line_obj.write(cr, uid, [move.sale_line_id.id], {'invoiced': True,
1051
'invoice_lines': [(4, line_id)],})
1055
def validate(self, cr, uid, ids, context=None):
1057
validate the shipment
1059
change the state to Done for the corresponding packing
1060
- validate the workflow for all the packings
1062
pick_obj = self.pool.get('stock.picking')
1063
wf_service = netsvc.LocalService("workflow")
1065
for shipment in self.browse(cr, uid, ids, context=context):
1066
# validate should only be called on shipped shipments
1067
assert shipment.state in ('shipped',), 'shipment state is not shipped'
1068
# corresponding packing objects - only the distribution -> customer ones
1069
# we have to discard picking object with state done, because when we return from shipment
1070
# all object of a given picking object, he is set to Done and still belong to the same shipment_id
1071
# another possibility would be to unlink the picking object from the shipment, set shipment_id to False
1072
# but in this case the returned pack families would not be displayed anymore in the shipment
1073
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '!=', 'done'),], context=context)
1075
for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
1076
assert packing.subtype == 'packing' and packing.state == 'assigned'
1077
# trigger standard workflow
1078
pick_obj.action_move(cr, uid, [packing.id])
1079
wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_done', cr)
1081
# Create automatically the invoice
1082
self.shipment_create_invoice(cr, uid, shipment.id, context=context)
1084
# log validate action
1085
self.log(cr, uid, shipment.id, _('The Shipment %s has been closed.')%(shipment.name,))
1087
result = self.complete_finished(cr, uid, ids, context=context)
1090
def set_delivered(self, cr, uid, ids, context=None):
1092
set the delivered flag
1095
pick_obj = self.pool.get('stock.picking')
1096
for shipment in self.browse(cr, uid, ids, context=context):
1097
# validate should only be called on shipped shipments
1098
assert shipment.state in ['done'], 'shipment state is not shipped'
1099
# gather the corresponding packing and trigger the corresponding function
1100
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '=', 'done')], context=context)
1101
# set delivered all packings
1102
pick_obj.set_delivered(cr, uid, packing_ids, context=context)
1109
class pack_family_memory(osv.osv_memory):
1111
dynamic memory object for pack families
1113
_name = 'pack.family.memory'
1115
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
1117
get functional values
1120
for pf_memory in self.browse(cr, uid, ids, context=context):
1121
values = {'move_lines': [],
1123
'location_id': False,
1124
'location_dest_id': False,
1125
'total_amount': 0.0,
1127
'currency_id': False,
1129
'total_weight': 0.0,
1130
'total_volume': 0.0,
1132
result[pf_memory.id] = values
1133
# pack family related fields
1134
if pf_memory.to_pack == 0:
1137
num_of_packs = pf_memory.to_pack - pf_memory.from_pack + 1
1138
values['num_of_packs'] = num_of_packs
1139
values['total_weight'] = pf_memory.weight * num_of_packs
1140
values['total_volume'] = (pf_memory.length * pf_memory.width * pf_memory.height * num_of_packs) / 1000.0
1142
# moves related fields
1143
for move in pf_memory.draft_packing_id.move_lines:
1144
if move.from_pack == pf_memory.from_pack:
1145
if move.to_pack == pf_memory.to_pack:
1146
# this move is in the good packing object and corresponds to this pack family
1147
# we add it to the stock move list
1148
values['move_lines'].append(move.id)
1149
values['state'] = move.state
1150
values['location_id'] = move.location_id.id
1151
values['location_dest_id'] = move.location_dest_id.id
1152
values['total_amount'] += move.total_amount
1153
values['amount'] += move.amount
1154
values['currency_id'] = move.currency_id and move.currency_id.id or False
1156
# when multiple moves are modified from/to values, the first one would raise an exception as the second one is not written yet
1158
#raise osv.except_osv(_('Error !'), _('Integrity check failed! Pack Family and Stock Moves from/to do not match.'))
1162
_columns = {'name': fields.char(string='Reference', size=1024),
1163
'shipment_id': fields.many2one('shipment', string='Shipment'),
1164
'draft_packing_id': fields.many2one('stock.picking', string="Draft Packing Ref"),
1165
'sale_order_id': fields.many2one('sale.order', string="Sale Order Ref"),
1166
'ppl_id': fields.many2one('stock.picking', string="PPL Ref"),
1167
'from_pack': fields.integer(string='From p.'),
1168
'to_pack': fields.integer(string='To p.'),
1169
'pack_type': fields.many2one('pack.type', string='Pack Type'),
1170
'length' : fields.float(digits=(16,2), string='Length [cm]'),
1171
'width' : fields.float(digits=(16,2), string='Width [cm]'),
1172
'height' : fields.float(digits=(16,2), string='Height [cm]'),
1173
'weight' : fields.float(digits=(16,2), string='Weight p.p [kg]'),
1175
'move_lines': fields.function(_vals_get, method=True, type='one2many', relation='stock.move', string='Stock Moves', multi='get_vals',),
1176
'state': fields.function(_vals_get, method=True, type='selection', selection=[('draft', 'Draft'),
1177
('assigned', 'Available'),
1178
('stock_return', 'Returned to Stock'),
1179
('ship_return', 'Returned from Shipment'),
1180
('cancel', 'Cancelled'),
1181
('done', 'Closed'),], string='State', multi='get_vals',),
1182
'location_id': fields.function(_vals_get, method=True, type='many2one', relation='stock.location', string='Src Loc.', multi='get_vals',),
1183
'location_dest_id': fields.function(_vals_get, method=True, type='many2one', relation='stock.location', string='Dest. Loc.', multi='get_vals',),
1184
'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', multi='get_vals',),
1185
'amount': fields.function(_vals_get, method=True, type='float', string='Pack Amount', multi='get_vals',),
1186
'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
1187
'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals',),
1188
'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals',),
1189
'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals',),
1190
'description_ppl': fields.char('Description', size=256 ),
1193
_defaults = {'shipment_id': False,
1194
'draft_packing_id': False,
1197
pack_family_memory()
1200
class shipment2(osv.osv):
1204
_inherit = 'shipment'
1206
def on_change_partner(self, cr, uid, ids, partner_id, address_id, context=None):
1208
Change the delivery address when the partner change.
1214
v.update({'address_id': False})
1216
d.update({'address_id': [('partner_id', '=', partner_id)]})
1220
addr = self.pool.get('res.partner.address').browse(cr, uid, address_id, context=context)
1222
if not address_id or addr.partner_id.id != partner_id:
1223
addr = self.pool.get('res.partner').address_get(cr, uid, partner_id, ['delivery', 'default'])
1224
if not addr.get('delivery'):
1225
addr = addr.get('default')
1227
addr = addr.get('delivery')
1229
v.update({'address_id': addr})
1235
def _vals_get_2(self, cr, uid, ids, fields, arg, context=None):
1237
get functional values
1239
picking_obj = self.pool.get('stock.picking')
1242
for shipment in self.browse(cr, uid, ids, context=context):
1243
values = {'pack_family_memory_ids':[],
1245
result[shipment.id] = values
1246
# look for all corresponding packing
1247
packing_ids = picking_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
1248
# get the corresponding data
1249
data = picking_obj.generate_data_from_picking_for_pack_family(cr, uid, packing_ids, context=context)
1250
# create a memory family
1251
created_ids = picking_obj.create_pack_families_memory_from_data(cr, uid, data, shipment.id, context=context)
1252
values['pack_family_memory_ids'].extend(created_ids)
1256
_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',),
1262
class ppl_customize_label(osv.osv):
1266
_name = 'ppl.customize.label'
1270
Load msf_outgoing_data.xml before self
1272
if hasattr(super(ppl_customize_label, self), 'init'):
1273
super(ppl_customize_label, self).init(cr)
1275
mod_obj = self.pool.get('ir.module.module')
1276
logging.getLogger('init').info('HOOK: module msf_outgoing: loading data/msf_outgoing_data.xml')
1277
pathname = path.join('msf_outgoing', 'data/msf_outgoing_data.xml')
1278
file = tools.file_open(pathname)
1279
tools.convert_xml_import(cr, 'msf_outgoing', file, {}, mode='init', noupdate=False)
1281
_columns = {'name': fields.char(string='Name', size=1024,),
1282
'notes': fields.text(string='Notes'),
1283
#'packing_list_reference': fields.boolean(string='Packing List Reference'),
1284
'pre_packing_list_reference': fields.boolean(string='Pre-Packing List Reference'),
1285
'destination_partner': fields.boolean(string='Destination Partner'),
1286
'destination_address': fields.boolean(string='Destination Address'),
1287
'requestor_order_reference': fields.boolean(string='Requestor Order Reference'),
1288
'weight': fields.boolean(string='Weight'),
1289
#'shipment_reference': fields.boolean(string='Shipment Reference'),
1290
'packing_parcel_number': fields.boolean(string='Packing Parcel Number'),
1291
#'expedition_parcel_number': fields.boolean(string='Expedition Parcel Number'),
1292
'specific_information': fields.boolean(string='Specific Information'),
1293
'logo': fields.boolean(string='Company Logo'),
1296
_defaults = {'name': 'My Customization',
1298
#'packing_list_reference': True,
1299
'pre_packing_list_reference': True,
1300
'destination_partner': True,
1301
'destination_address': True,
1302
'requestor_order_reference': True,
1304
#'shipment_reference': True,
1305
'packing_parcel_number': True,
1306
#'expedition_parcel_number': True,
1307
'specific_information': True,
1311
ppl_customize_label()
1314
class stock_picking(osv.osv):
1316
override stock picking to add new attributes
1317
- flow_type: the type of flow (full, quick)
1318
- subtype: the subtype of picking object (picking, ppl, packing)
1319
- previous_step_id: the id of picking object of the previous step, picking for ppl, ppl for packing
1321
_inherit = 'stock.picking'
1322
_name = 'stock.picking'
1324
def fields_view_get(self, cr, uid, view_id, view_type, context=None, toolbar=False, submenu=False):
1326
Set the appropriate search view according to the context
1331
if not view_id and context.get('wh_dashboard') and view_type == 'search':
1333
if context.get('pick_type') == 'incoming':
1334
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'view_picking_in_search')[1]
1335
elif context.get('pick_type') == 'delivery':
1336
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'view_picking_out_search')[1]
1337
elif context.get('pick_type') == 'picking_ticket':
1338
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_search')[1]
1339
elif context.get('pick_type') == 'pack':
1340
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_search')[1]
1344
return super(stock_picking, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
1346
def unlink(self, cr, uid, ids, context=None):
1348
unlink test for draft
1350
datas = self.read(cr, uid, ids, ['state','type','subtype'], context=context)
1351
if [data for data in datas if data['state'] != 'draft']:
1352
raise osv.except_osv(_('Warning !'), _('Only draft picking tickets can be deleted.'))
1353
ids_picking_draft = [data['id'] for data in datas if data['subtype'] == 'picking' and data['type'] == 'out' and data['state'] == 'draft']
1354
if ids_picking_draft:
1355
data = self.has_picking_ticket_in_progress(cr, uid, ids, context=context)
1356
if [x for x in data.values() if x]:
1357
raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try again.'))
1359
return super(stock_picking, self).unlink(cr, uid, ids, context=context)
1361
def _hook_picking_get_view(self, cr, uid, ids, context=None, *args, **kwargs):
1362
pick = kwargs['pick']
1363
obj_data = self.pool.get('ir.model.data')
1364
view_list = {'standard': ('stock', 'view_picking_out_form'),
1365
'picking': ('msf_outgoing', 'view_picking_ticket_form'),
1366
'ppl': ('msf_outgoing', 'view_ppl_form'),
1367
'packing': ('msf_outgoing', 'view_packing_form'),
1369
if pick.type == 'out':
1370
context.update({'picking_type': pick.subtype == 'standard' and 'delivery_order' or 'picking_ticket'})
1371
module, view = view_list.get(pick.subtype,('msf_outgoing', 'view_picking_ticket_form'))
1373
return obj_data.get_object_reference(cr, uid, module, view)
1374
except ValueError, e:
1376
elif pick.type == 'in':
1377
context.update({'picking_type': 'incoming_shipment'})
1379
context.update({'picking_type': 'internal_move'})
1381
return super(stock_picking, self)._hook_picking_get_view(cr, uid, ids, context=context, *args, **kwargs)
1383
def _hook_custom_log(self, cr, uid, ids, context=None, *args, **kwargs):
1385
hook from stock>stock.py>log_picking
1386
update the domain and other values if necessary in the log creation
1388
result = super(stock_picking, self)._hook_custom_log(cr, uid, ids, context=context, *args, **kwargs)
1389
pick_obj = self.pool.get('stock.picking')
1390
pick = kwargs['pick']
1391
message = kwargs['message']
1392
if pick.type and pick.subtype:
1393
domain = [('type', '=', pick.type), ('subtype', '=', pick.subtype)]
1394
return self.pool.get('res.log').create(cr, uid,
1396
'res_model': pick_obj._name,
1403
def _hook_log_picking_log_cond(self, cr, uid, ids, context=None, *args, **kwargs):
1405
hook from stock>stock.py>stock_picking>log_picking
1406
specify if we display a log or not
1408
result = super(stock_picking, self)._hook_log_picking_log_cond(cr, uid, ids, context=context, *args, **kwargs)
1409
pick = kwargs['pick']
1410
if pick.subtype == 'packing':
1412
# if false the log will be defined by the method _hook_custom_log (which include a domain)
1413
if pick.type and pick.subtype:
1418
def copy(self, cr, uid, id, default=None, context=None):
1420
set the name corresponding to object subtype
1426
obj = self.browse(cr, uid, id, context=context)
1427
if not context.get('allow_copy', False):
1428
if obj.subtype == 'picking':
1429
if not obj.backorder_id:
1431
default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket'),
1433
date=date.today().strftime('%Y-%m-%d'),
1437
# if the corresponding draft picking ticket is done, we do not allow copy
1438
if obj.backorder_id and obj.backorder_id.state == 'done':
1439
raise osv.except_osv(_('Error !'), _('Corresponding Draft picking ticket is Closed. This picking ticket cannot be copied.'))
1440
# picking ticket, use draft sequence, keep other fields
1442
base = base.split('-')[0] + '-'
1443
default.update(name=base + obj.backorder_id.sequence_id.get_id(test='id', context=context),
1444
date=date.today().strftime('%Y-%m-%d'),
1447
elif obj.subtype == 'ppl':
1448
raise osv.except_osv(_('Error !'), _('Pre-Packing List copy is forbidden.'))
1449
# ppl, use the draft picking ticket sequence
1450
# if obj.previous_step_id and obj.previous_step_id.backorder_id:
1452
# base = base.split('-')[0] + '-'
1453
# default.update(name=base + obj.previous_step_id.backorder_id.sequence_id.get_id(test='id', context=context))
1455
# default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'ppl'))
1457
result = super(stock_picking, self).copy(cr, uid, id, default=default, context=context)
1458
if not context.get('allow_copy', False):
1459
if obj.subtype == 'picking' and obj.backorder_id:
1460
# confirm the new picking ticket - the picking ticket should not stay in draft state !
1461
wf_service = netsvc.LocalService("workflow")
1462
wf_service.trg_validate(uid, 'stock.picking', result, 'button_confirm', cr)
1463
# we force availability
1464
self.force_assign(cr, uid, [result])
1467
def copy_data(self, cr, uid, id, default=None, context=None):
1469
reset one2many fields
1475
# reset one2many fields
1476
default.update(backorder_ids=[])
1477
default.update(previous_step_ids=[])
1478
default.update(pack_family_memory_ids=[])
1480
context['not_workflow'] = True
1481
result = super(stock_picking, self).copy_data(cr, uid, id, default=default, context=context)
1485
def _erase_prodlot_hook(self, cr, uid, id, context=None, *args, **kwargs):
1487
hook to keep the production lot when a stock move is copied
1489
res = super(stock_picking, self)._erase_prodlot_hook(cr, uid, id, context=context, *args, **kwargs)
1491
return res and not context.get('keep_prodlot', False)
1493
def has_picking_ticket_in_progress(self, cr, uid, ids, context=None):
1495
ids is the list of draft picking object we want to test
1496
completed means, we recursively check that next_step link object is cancel or done
1498
return true if picking tickets are in progress, meaning picking ticket or ppl or shipment not done exist
1502
if isinstance(ids, (int, long)):
1505
for obj in self.browse(cr, uid, ids, context=context):
1506
# by default, nothing is in progress
1508
# treat only draft picking
1509
assert obj.subtype in 'picking' and obj.state == 'draft', 'the validate function should only be called on draft picking ticket objects'
1510
for picking in obj.backorder_ids:
1511
# take care, is_completed returns a dictionary
1512
if not picking.is_completed()[picking.id]:
1518
def validate(self, cr, uid, ids, context=None):
1520
validate or not the draft picking ticket
1523
move_obj = self.pool.get('stock.move')
1525
for draft_picking in self.browse(cr, uid, ids, context=context):
1526
# the validate function should only be called on draft picking ticket
1527
assert draft_picking.subtype == 'picking' and draft_picking.state == 'draft', 'the validate function should only be called on draft picking ticket objects'
1528
#check the qty of all stock moves
1530
move_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_picking.id),
1531
('product_qty', '!=', 0.0),
1532
('state', 'not in', ['done', 'cancel'])], context=context)
1537
# then all child picking must be fully completed, meaning:
1538
# - all picking must be 'completed'
1539
# completed means, we recursively check that next_step link object is cancel or done
1540
if self.has_picking_ticket_in_progress(cr, uid, [draft_picking.id], context=context)[draft_picking.id]:
1544
# - all picking are completed (means ppl completed and all shipment validated)
1545
wf_service = netsvc.LocalService("workflow")
1546
wf_service.trg_validate(uid, 'stock.picking', draft_picking.id, 'button_confirm', cr)
1547
# we force availability
1548
draft_picking.force_assign()
1550
draft_picking.action_move()
1551
wf_service.trg_validate(uid, 'stock.picking', draft_picking.id, 'button_done', cr)
1555
def _vals_get_2(self, cr, uid, ids, fields, arg, context=None):
1557
get functional values
1560
for stock_picking in self.browse(cr, uid, ids, context=context):
1561
values = {'pack_family_memory_ids':[],
1563
result[stock_picking.id] = values
1565
# get the corresponding data for pack family memory
1566
data = self.generate_data_from_picking_for_pack_family(cr, uid, [stock_picking.id], context=context)
1567
# create a memory family - no shipment id
1568
created_ids = self.create_pack_families_memory_from_data(cr, uid, data, shipment_id=False, context=context)
1569
values['pack_family_memory_ids'].extend(created_ids)
1573
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
1575
get functional values
1578
for stock_picking in self.browse(cr, uid, ids, context=context):
1579
values = {'total_amount': 0.0,
1580
'currency_id': False,
1581
'is_dangerous_good': False,
1582
'is_keep_cool': False,
1583
'is_narcotic': False,
1585
'total_volume': 0.0,
1586
'total_weight': 0.0,
1587
#'is_completed': False,
1590
result[stock_picking.id] = values
1592
for family in stock_picking.pack_family_memory_ids:
1593
# number of packs from pack_family
1594
num_of_packs = family.num_of_packs
1595
values['num_of_packs'] += int(num_of_packs)
1597
total_weight = family.total_weight
1598
values['total_weight'] += total_weight
1599
total_volume = family.total_volume
1600
values['total_volume'] += total_volume
1602
for move in stock_picking.move_lines:
1603
# total amount (float)
1604
total_amount = move.total_amount
1605
values['total_amount'] = total_amount
1607
values['currency_id'] = move.currency_id and move.currency_id.id or False
1609
values['is_dangerous_good'] = move.is_dangerous_good
1610
# keep cool - if heat_sensitive_item is True
1611
values['is_keep_cool'] = move.is_keep_cool
1613
values['is_narcotic'] = move.is_narcotic
1614
# overall qty of products in all corresponding stock moves
1615
values['overall_qty'] += move.product_qty
1617
# completed field - based on the previous_step_ids field, recursive call from picking to draft packing and packing
1618
# - picking checks that the corresponding ppl is completed
1619
# - ppl checks that the corresponding draft packing and packings are completed
1620
# the recursion stops there because packing does not have previous_step_ids values
1621
# completed = stock_picking.state in ('done', 'cancel')
1623
# for next_step in stock_picking.previous_step_ids:
1624
# if not next_step.is_completed:
1628
# values['is_completed'] = completed
1632
def is_completed(self, cr, uid, ids, context=None):
1634
recursive test of completion
1635
- to be applied on picking ticket
1638
for picking in draft_picking.backorder_ids:
1639
# take care, is_completed returns a dictionary
1640
if not picking.is_completed()[picking.id]:
1643
***BEWARE: RETURNS A DICTIONARY !
1646
for stock_picking in self.browse(cr, uid, ids, context=context):
1648
state = stock_picking.state
1649
subtype = stock_picking.subtype
1650
completed = stock_picking.state in ('done', 'cancel')
1651
result[stock_picking.id] = completed
1653
for next_step in stock_picking.previous_step_ids:
1654
if not next_step.is_completed()[next_step.id]:
1656
result[stock_picking.id] = completed
1663
Load msf_outgoing_data.xml before self
1665
if hasattr(super(stock_picking, self), 'init'):
1666
super(stock_picking, self).init(cr)
1668
mod_obj = self.pool.get('ir.module.module')
1670
mod_id = mod_obj.search(cr, 1, [('name', '=', 'msf_outgoing'),])
1672
demo = mod_obj.read(cr, 1, mod_id, ['demo'])[0]['demo']
1675
logging.getLogger('init').info('HOOK: module msf_outgoing: loading data/msf_outgoing_data.xml')
1676
pathname = path.join('msf_outgoing', 'data/msf_outgoing_data.xml')
1677
file = tools.file_open(pathname)
1678
tools.convert_xml_import(cr, 'msf_outgoing', file, {}, mode='init', noupdate=False)
1680
def _qty_search(self, cr, uid, obj, name, args, context=None):
1681
""" Searches Ids of stock picking
1682
@return: Ids of locations
1687
stock_pickings = self.pool.get('stock.picking').search(cr, uid, [], context=context)
1690
for stock_picking in self.browse(cr, uid, stock_pickings, context=context):
1691
result[stock_picking.id] = 0.0
1692
for move in stock_picking.move_lines:
1693
result[stock_picking.id] += move.product_qty
1694
# construct the request
1695
# adapt the operator
1699
ids = [('id', 'in', [x for x in result.keys() if eval("%s %s %s"%(result[x], op, args[0][2]))])]
1702
def _get_picking_ids(self, cr, uid, ids, context=None):
1704
ids represents the ids of stock.move objects for which values have changed
1705
return the list of ids of picking object which need to get their state field updated
1707
self is stock.move object
1710
for obj in self.browse(cr, uid, ids, context=context):
1711
if obj.picking_id and obj.picking_id.id not in result:
1712
result.append(obj.picking_id.id)
1715
_columns = {'flow_type': fields.selection([('full', 'Full'),('quick', 'Quick')], readonly=True, states={'draft': [('readonly', False),],}, string='Flow Type'),
1716
'subtype': fields.selection([('standard', 'Standard'), ('picking', 'Picking'),('ppl', 'PPL'),('packing', 'Packing')], string='Subtype'),
1717
'backorder_ids': fields.one2many('stock.picking', 'backorder_id', string='Backorder ids',),
1718
'previous_step_id': fields.many2one('stock.picking', 'Previous step'),
1719
'previous_step_ids': fields.one2many('stock.picking', 'previous_step_id', string='Previous Step ids',),
1720
'shipment_id': fields.many2one('shipment', string='Shipment'),
1721
'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'),
1722
'first_shipment_packing_id': fields.many2one('stock.picking', 'Shipment First Step'),
1723
#'pack_family_ids': fields.one2many('pack.family', 'ppl_id', string='Pack Families',),
1724
# attributes for specific packing labels
1725
'ppl_customize_label': fields.many2one('ppl.customize.label', string='Labels Customization',),
1726
# warehouse info (locations) are gathered from here - allow shipment process without sale order
1727
'warehouse_id': fields.many2one('stock.warehouse', string='Warehouse', required=True,),
1728
# flag for converted picking
1729
'converted_to_standard': fields.boolean(string='Converted to Standard'),
1731
'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals_X'), # old_multi get_vals
1732
'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals'),
1733
'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals'),
1734
'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals'),
1735
'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals'),
1736
'is_dangerous_good': fields.function(_vals_get, method=True, type='boolean', string='Dangerous Good', multi='get_vals'),
1737
'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals'),
1738
'is_narcotic': fields.function(_vals_get, method=True, type='boolean', string='Narcotic', multi='get_vals'),
1739
'overall_qty': fields.function(_vals_get, method=True, fnct_search=_qty_search, type='float', string='Overall Qty', multi='get_vals',
1740
store= {'stock.move': (_get_picking_ids, ['product_qty', 'picking_id'], 10),}),
1741
#'is_completed': fields.function(_vals_get, method=True, type='boolean', string='Completed Process', multi='get_vals',),
1742
'pack_family_memory_ids': fields.function(_vals_get_2, method=True, type='one2many', relation='pack.family.memory', string='Memory Families', multi='get_vals_2',),
1743
'description_ppl': fields.char('Description', size=256 ),
1745
_defaults = {'flow_type': 'full',
1746
'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,
1747
'subtype': 'standard',
1748
'first_shipment_packing_id': False,
1749
'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,
1750
'converted_to_standard': False,
1752
#_order = 'origin desc, name asc'
1753
_order = 'name desc'
1755
def picking_ticket_data(self, cr, uid, ids, context=None):
1757
generate picking ticket data for report creation
1759
- sale order line without product: does not work presently
1761
- many sale order line with same product: stored in different dictionary with line id as key.
1762
so the same product could be displayed many times in the picking ticket according to sale order
1764
- many stock move with same product: two cases, if from different sale order lines, the above rule applies,
1765
if from the same order line, they will be stored according to prodlot id
1767
- many stock move with same prodlot (so same product): if same sale order line, the moves will be
1768
stored in the same structure, with global quantity, i.e. this batch for this product for this
1769
sale order line will be displayed only once with summed quantity from concerned stock moves
1771
[sale_line.id][product_id][prodlot_id]
1773
other prod lot, not used are added in order that all prod lot are displayed
1775
to check, if a move does not come from the sale order line:
1776
stored with line id False, product is relevant, multiple
1777
product for the same 0 line id is possible
1780
for stock_picking in self.browse(cr, uid, ids, context=context):
1782
result[stock_picking.id] = {'obj': stock_picking,
1785
for move in stock_picking.move_lines:
1786
if move.product_id: # product is mandatory at stock_move level ;)
1787
sale_line_id = move.sale_line_id and move.sale_line_id.id or False
1788
# structure, data is reorganized in order to regroup according to sale order line > product > production lot
1789
# and to sum the quantities corresponding to different levels because this is impossible within the rml framework
1791
.setdefault(sale_line_id, {}) \
1792
.setdefault('products', {}) \
1793
.setdefault(move.product_id.id, {}) \
1794
.setdefault('uoms', {}) \
1795
.setdefault(move.product_uom.id, {}) \
1796
.setdefault('lots', {})
1798
# ** sale order line info**
1799
values[sale_line_id]['obj'] = move.sale_line_id or False
1801
# **uom level info**
1802
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['obj'] = move.product_uom
1804
# **prodlot level info**
1806
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'].setdefault(move.prodlot_id.id, {})
1807
# qty corresponding to this production lot
1808
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id].setdefault('reserved_qty', 0)
1809
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id]['reserved_qty'] += move.product_qty
1810
# store the object for info retrieval
1811
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id]['obj'] = move.prodlot_id
1813
# **product level info**
1814
# total quantity from STOCK_MOVES for one sale order line (directly for one product)
1815
# or if not linked to a sale order line, stock move created manually, the line id is False
1816
# and in this case the product is important
1817
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id].setdefault('qty_to_pick_sm', 0)
1818
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['qty_to_pick_sm'] += move.product_qty
1819
# total quantity from SALE_ORDER_LINES, which can be different from the one from stock moves
1820
# if stock moves have been created manually in the picking, no present in the so, equal to 0 if not linked to an so
1821
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id].setdefault('qty_to_pick_so', 0)
1822
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
1823
# store the object for info retrieval
1824
values[sale_line_id]['products'][move.product_id.id]['obj'] = move.product_id
1826
# all moves have been treated
1827
# complete the lot lists for each product
1828
for sale_line in values.values():
1829
for product in sale_line['products'].values():
1830
for uom in product['uoms'].values():
1831
# loop through all existing production lot for this product - all are taken into account, internal and external
1832
for lot in product['obj'].prodlot_ids:
1833
if lot.id not in uom['lots'].keys():
1834
# the lot is not present, we add it
1835
uom['lots'][lot.id] = {}
1836
uom['lots'][lot.id]['obj'] = lot
1837
# reserved qty is 0 since no stock moves correspond to this lot
1838
uom['lots'][lot.id]['reserved_qty'] = 0.0
1842
def create_sequence(self, cr, uid, vals, context=None):
1844
Create new entry sequence for every new picking
1845
@param cr: cursor to database
1846
@param user: id of current user
1847
@param ids: list of record ids to be process
1848
@param context: context arguments, like lang, time zone
1849
@return: return a result
1851
example of name: 'PICK/xxxxx'
1852
example of code: 'picking.xxxxx'
1853
example of prefix: 'PICK'
1854
example of padding: 5
1856
seq_pool = self.pool.get('ir.sequence')
1857
seq_typ_pool = self.pool.get('ir.sequence.type')
1859
default_name = 'Stock Picking'
1860
default_code = 'stock.picking'
1867
name = vals.get('name', False)
1870
code = vals.get('code', False)
1873
prefix = vals.get('prefix', False)
1875
prefix = default_prefix
1876
padding = vals.get('padding', False)
1878
padding = default_padding
1884
seq_typ_pool.create(cr, uid, types)
1892
return seq_pool.create(cr, uid, seq)
1894
def generate_data_from_picking_for_pack_family(self, cr, uid, pick_ids, object_type='shipment', from_pack=False, to_pack=False, context=None):
1896
generate the data structure from the stock.picking object
1898
we can limit the generation to certain from/to sequence
1900
one data for each move_id - here is the difference with data generated from partial
1903
{pick_id: {from_pack: {to_pack: {move_id: {data}}}}}
1905
if the move has a quantity equal to 0, it means that no pack are available,
1906
these moves are therefore not taken into account for the pack families generation
1908
TODO: integrity constraints
1910
Note: why the same dictionary is repeated n times for n moves, because
1911
it is directly used when we create the pack families. could be refactored
1912
with one dic per from/to with a 'move_ids' entry
1914
assert bool(from_pack) == bool(to_pack), 'from_pack and to_pack must be either both filled or empty'
1918
if object_type == 'shipment':
1919
# all moves are taken into account, therefore the back moves are represented
1920
# by done pack families
1922
elif object_type == 'memory':
1923
# done moves are not displayed as pf as we cannot select these packs anymore (they are returned)
1926
assert False, 'Should not reach this line'
1928
for pick in self.browse(cr, uid, pick_ids, context=context):
1929
result[pick.id] = {}
1930
for move in pick.move_lines:
1931
if not from_pack or move.from_pack == from_pack:
1932
if not to_pack or move.to_pack == to_pack:
1933
# the quantity must be positive and the state depends on the window's type
1934
if move.product_qty and move.state not in states:
1935
# subtype == ppl - called from stock picking
1936
if pick.subtype == 'ppl':
1938
.setdefault(move.from_pack, {}) \
1939
.setdefault(move.to_pack, {})[move.id] = {'sale_order_id': pick.sale_id.id,
1940
'ppl_id': pick.id, # only change between ppl - packing
1941
'from_pack': move.from_pack,
1942
'to_pack': move.to_pack,
1943
'pack_type': move.pack_type.id,
1944
'length': move.length,
1945
'width': move.width,
1946
'height': move.height,
1947
'weight': move.weight,
1948
'draft_packing_id': pick.id,
1949
'description_ppl': pick.description_ppl,
1951
# subtype == packing - caled from shipment
1952
elif pick.subtype == 'packing':
1954
.setdefault(move.from_pack, {}) \
1955
.setdefault(move.to_pack, {})[move.id] = {'sale_order_id': pick.sale_id.id,
1956
'ppl_id': pick.previous_step_id.id,
1957
'from_pack': move.from_pack,
1958
'to_pack': move.to_pack,
1959
'pack_type': move.pack_type.id,
1960
'length': move.length,
1961
'width': move.width,
1962
'height': move.height,
1963
'weight': move.weight,
1964
'draft_packing_id': pick.id,
1967
if object_type != 'memory':
1968
result[pick.id][move.from_pack][move.to_pack][move.id]['description_ppl'] = pick.description_ppl
1972
def create_pack_families_memory_from_data(self, cr, uid, data, shipment_id, context=None,):
1974
- clear existing pack family memory objects is not necessary thanks to vaccum system
1975
-> in fact cleaning old memory objects reslults in a bug, because when we click on a
1976
pf memory to see it's form view, the shipment view is regenerated (delete the pf)
1977
but then the form view uses the old pf memory id for data and it crashes. Cleaning
1978
of previous pf memory has therefore been removed.
1979
- generate new ones based on data
1980
- return created ids
1982
pf_memory_obj = self.pool.get('pack.family.memory')
1983
# find and delete existing objects
1984
#ids = pf_memory_obj.search(cr, uid, [('shipment_id', '=', shipment_id),], context=context)
1985
#pf_memory_obj.unlink(cr, uid, ids, context=context)
1987
# create pack family memory
1988
for picking in data.values():
1989
for from_pack in picking.values():
1990
for to_pack in from_pack.values():
1991
for move_id in to_pack.keys():
1992
move_data = to_pack[move_id]
1993
# create corresponding memory object
1994
move_data.update(name='_name',
1995
shipment_id=shipment_id,)
1996
id = pf_memory_obj.create(cr, uid, move_data, context=context)
1997
created_ids.append(id)
2000
def create(self, cr, uid, vals, context=None):
2002
creation of a stock.picking of subtype 'packing' triggers
2004
- creation of corresponding shipment
2006
# For picking ticket from scratch, invoice it !
2007
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':
2008
vals['invoice_state'] = '2binvoiced'
2010
date_tools = self.pool.get('date.tools')
2011
fields_tools = self.pool.get('fields.tools')
2012
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
2013
db_datetime_format = date_tools.get_db_datetime_format(cr, uid, context=context)
2017
# the action adds subtype in the context depending from which screen it is created
2018
if context.get('picking_screen', False) and not vals.get('name', False):
2019
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket')
2020
vals.update(subtype='picking',
2026
if context.get('ppl_screen', False) and not vals.get('name', False):
2027
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'ppl')
2028
vals.update(subtype='ppl',
2034
shipment_obj = self.pool.get('shipment')
2036
move_obj = self.pool.get('stock.move')
2040
if 'subtype' in vals and vals['subtype'] == 'picking':
2041
# creation of a new picking ticket
2042
assert 'backorder_id' in vals, 'No backorder_id'
2044
if not vals['backorder_id']:
2045
# creation of *draft* picking ticket
2046
vals.update(sequence_id=self.create_sequence(cr, uid, {'name':vals['name'],
2047
'code':vals['name'],
2049
'padding':2}, context=context))
2051
if 'subtype' in vals and vals['subtype'] == 'packing':
2052
# creation of a new packing
2053
assert 'backorder_id' in vals, 'No backorder_id'
2054
assert 'shipment_id' in vals, 'No shipment_id'
2056
if not vals['backorder_id']:
2057
# creation of *draft* picking ticket
2058
vals.update(sequence_id=self.create_sequence(cr, uid, {'name':vals['name'],
2059
'code':vals['name'],
2062
}, context=context))
2064
# create packing object
2065
new_packing_id = super(stock_picking, self).create(cr, uid, vals, context=context)
2067
if 'subtype' in vals and vals['subtype'] == 'packing':
2068
# creation of a new packing
2069
assert 'backorder_id' in vals, 'No backorder_id'
2070
assert 'shipment_id' in vals, 'No shipment_id'
2072
if vals['backorder_id'] and vals['shipment_id']:
2073
# ship of existing shipment
2076
return new_packing_id
2078
if vals['backorder_id'] and not vals['shipment_id']:
2079
# data from do_create_shipment method
2080
assert 'partial_datas_shipment' in context, 'Missing partial_datas_shipment'
2081
assert 'draft_shipment_id' in context, 'Missing draft_shipment_id'
2082
assert 'draft_packing_id' in context, 'Missing draft_packing_id'
2083
assert 'shipment_id' in context, 'Missing shipment_id'
2084
draft_shipment_id = context['draft_shipment_id']
2085
draft_packing_id = context['draft_packing_id']
2086
data = context['partial_datas_shipment'][draft_shipment_id][draft_packing_id]
2087
shipment_id = context['shipment_id']
2088
# We have a backorder_id, no shipment_id
2089
# -> we have just created a shipment
2090
# the created packing object has no stock_move
2091
# - we create the sock move from the data in context
2092
# - if no shipment in context, create a new shipment object
2093
# - generate the data from the new picking object
2094
# - create the pack families
2095
for from_pack in data:
2096
for to_pack in data[from_pack]:
2097
# total number of packs
2098
total_num = to_pack - from_pack + 1
2099
# number of selected packs to ship
2100
# note: when the data is generated, lines without selected_number are not kept, so we have nothing to check here
2101
selected_number = data[from_pack][to_pack][0]['selected_number']
2102
# we take the packs with the highest numbers
2104
selected_from_pack = to_pack - selected_number + 1
2105
selected_to_pack = to_pack
2106
# update initial moves
2107
if selected_number == total_num:
2108
# if all packs have been selected, from/to are set to 0
2109
initial_from_pack = 0
2112
initial_from_pack = from_pack
2113
initial_to_pack = to_pack - selected_number
2115
# find the corresponding moves
2116
moves_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_packing_id),
2117
('from_pack', '=', from_pack),
2118
('to_pack', '=', to_pack),], context=context)
2120
for move in move_obj.browse(cr, uid, moves_ids, context=context):
2121
# we compute the selected quantity
2122
selected_qty = move.qty_per_pack * selected_number
2123
# create the new move - store the back move from draft **packing** object
2124
new_move = move_obj.copy(cr, uid, move.id, {'picking_id': new_packing_id,
2125
'product_qty': selected_qty,
2126
'from_pack': selected_from_pack,
2127
'to_pack': selected_to_pack,
2128
'backmove_packing_id': move.id,}, context=context)
2130
# update corresponding initial move
2131
initial_qty = move.product_qty
2132
initial_qty = max(initial_qty - selected_qty, 0)
2133
# if all packs have been selected, from/to have been set to 0
2134
# update the original move object - the corresponding original shipment (draft)
2135
# is automatically updated generically in the write method
2136
move_obj.write(cr, uid, [move.id], {'product_qty': initial_qty,
2137
'from_pack': initial_from_pack,
2138
'to_pack': initial_to_pack}, context=context)
2140
if not vals['backorder_id']:
2141
# creation of packing after ppl validation
2142
# find an existing shipment or create one - depends on new pick state
2143
shipment_ids = shipment_obj.search(cr, uid, [('state', '=', 'draft'), ('address_id', '=', vals['address_id'])], context=context)
2144
# only one 'draft' shipment should be available
2145
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)
2146
# get rts of corresponding sale order
2147
sale_id = self.read(cr, uid, [new_packing_id], ['sale_id'], context=context)
2148
sale_id = sale_id[0]['sale_id']
2150
sale_id = sale_id[0]
2152
today = time.strftime(db_datetime_format)
2153
rts = self.pool.get('sale.order').read(cr, uid, [sale_id], ['ready_to_ship_date'], context=context)[0]['ready_to_ship_date']
2155
rts = date.today().strftime(db_date_format)
2157
shipment_lt = fields_tools.get_field_from_company(cr, uid, object=self._name, field='shipment_lead_time', context=context)
2158
rts_obj = datetime.strptime(rts, db_date_format)
2159
rts = rts_obj + relativedelta(days=shipment_lt or 0)
2160
rts = rts.strftime(db_date_format)
2162
if not len(shipment_ids):
2163
# no shipment, create one - no need to specify the state, it's a function
2164
name = self.pool.get('ir.sequence').get(cr, uid, 'shipment')
2165
addr = self.pool.get('res.partner.address').browse(cr, uid, vals['address_id'], context=context)
2166
partner_id = addr.partner_id and addr.partner_id.id or False
2167
values = {'name': name,
2168
'address_id': vals['address_id'],
2169
'partner_id2': partner_id,
2170
'shipment_expected_date': rts,
2171
'shipment_actual_date': rts,
2172
'sequence_id': self.create_sequence(cr, uid, {'name':name,
2175
'padding':2}, context=context)}
2177
shipment_id = shipment_obj.create(cr, uid, values, context=context)
2178
shipment_obj.log(cr, uid, shipment_id, _('The new Draft Shipment %s has been created.')%(name,))
2180
shipment_id = shipment_ids[0]
2181
shipment = shipment_obj.browse(cr, uid, shipment_id, context=context)
2182
# if expected ship date of shipment is greater than rts, update shipment_expected_date and shipment_actual_date
2183
shipment_expected = datetime.strptime(shipment.shipment_expected_date, db_datetime_format)
2184
if rts_obj < shipment_expected:
2185
shipment.write({'shipment_expected_date': rts, 'shipment_actual_date': rts,}, context=context)
2186
shipment_name = shipment.name
2187
shipment_obj.log(cr, uid, shipment_id, _('The ppl has been added to the existing Draft Shipment %s.')%(shipment_name,))
2189
# update the new pick with shipment_id
2190
self.write(cr, uid, [new_packing_id], {'shipment_id': shipment_id}, context=context)
2192
return new_packing_id
2194
def _hook_action_assign_raise_exception(self, cr, uid, ids, context=None, *args, **kwargs):
2196
Please copy this to your module's method also.
2197
This hook belongs to the action_assign method from stock>stock.py>stock_picking class
2199
- allow to choose wether or not an exception should be raised in case of no stock move
2201
res = super(stock_picking, self)._hook_action_assign_raise_exception(cr, uid, ids, context=context, *args, **kwargs)
2202
return res and False
2204
def _hook_log_picking_modify_message(self, cr, uid, ids, context=None, *args, **kwargs):
2206
stock>stock.py>log_picking
2207
update the message to be displayed by the function
2209
pick = kwargs['pick']
2210
message = kwargs['message']
2211
# if the picking is converted to standard, and state is confirmed
2212
if pick.converted_to_standard and pick.state == 'confirmed':
2213
return 'The Preparation Picking has been converted to simple Out. ' + message
2214
if pick.type == 'out' and pick.subtype == 'picking':
2215
kwargs['message'] = message.replace('Delivery Order', 'Picking Ticket')
2216
elif pick.type == 'out' and pick.subtype == 'packing':
2217
kwargs['message'] = message.replace('Delivery Order', 'Packing List')
2218
elif pick.type == 'out' and pick.subtype == 'ppl':
2219
kwargs['message'] = message.replace('Delivery Order', 'Pre-Packing List')
2220
return super(stock_picking, self)._hook_log_picking_modify_message(cr, uid, ids, context, *args, **kwargs)
2222
def convert_to_standard(self, cr, uid, ids, context=None):
2224
check of back orders exists, if not, convert to standard: change subtype to standard, and trigger workflow
2226
only one picking object at a time
2231
date_tools = self.pool.get('date.tools')
2232
fields_tools = self.pool.get('fields.tools')
2233
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
2234
for obj in self.browse(cr, uid, ids, context=context):
2235
# the convert function should only be called on draft picking ticket
2236
assert obj.subtype == 'picking' and obj.state == 'draft', 'the convert function should only be called on draft picking ticket objects'
2237
if self.has_picking_ticket_in_progress(cr, uid, [obj.id], context=context)[obj.id]:
2238
raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try again.'))
2240
# log a message concerning the conversion
2241
new_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
2242
self.log(cr, uid, obj.id, _('The Preparation Picking (%s) has been converted to simple Out (%s).')%(obj.name, new_name))
2243
# change subtype and name
2244
obj.write({'name': new_name,
2245
'subtype': 'standard',
2246
'converted_to_standard': True,
2248
# all destination location of the stock moves must be output location of warehouse - lot_output_id
2249
# if corresponding sale order, date and date_expected are updated to rts + shipment lt
2250
for move in obj.move_lines:
2251
# was previously set to confirmed/assigned, otherwise, when we confirm the stock picking,
2252
# using draft_force_assign, the moves are not treated because not in draft
2253
# and the corresponding chain location on location_dest_id was not computed
2254
# we therefore set them back in draft state before treatment
2255
if move.product_qty == 0.0:
2256
vals = {'state': 'done'}
2258
vals = {'state': 'draft'}
2259
# If the move comes from a DPO, don't change the destination location
2261
vals.update({'location_dest_id': obj.warehouse_id.lot_output_id.id})
2265
shipment_lt = fields_tools.get_field_from_company(cr, uid, object=self._name, field='shipment_lead_time', context=context)
2266
rts = datetime.strptime(obj.sale_id.ready_to_ship_date, db_date_format)
2267
rts = rts + relativedelta(days=shipment_lt or 0)
2268
rts = rts.strftime(db_date_format)
2269
vals.update({'date': rts, 'date_expected': rts, 'state': 'draft'})
2270
move.write(vals, context=context)
2271
if move.product_qty == 0.00:
2272
move.action_done(context=context)
2275
# trigger workflow (confirm picking)
2276
self.draft_force_assign(cr, uid, [obj.id])
2277
# check availability
2278
self.action_assign(cr, uid, [obj.id], context=context)
2280
# TODO which behavior
2281
data_obj = self.pool.get('ir.model.data')
2282
view_id = data_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
2283
view_id = view_id and view_id[1] or False
2284
context.update({'picking_type': 'delivery_order'})
2285
return {'name':_("Delivery Orders"),
2286
'view_mode': 'form,tree',
2287
'view_id': [view_id],
2288
'view_type': 'form',
2289
'res_model': 'stock.picking',
2291
'type': 'ir.actions.act_window',
2296
def create_picking(self, cr, uid, ids, context=None):
2298
open the wizard to create (partial) picking tickets
2300
# we need the context for the wizard switch
2305
name = _("Create Picking Ticket")
2306
model = 'create.picking'
2308
wiz_obj = self.pool.get('wizard')
2309
# open the selected wizard
2310
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2312
def do_create_picking_first_hook(self, cr, uid, ids, context, *args, **kwargs):
2314
hook to update new_move data. Originally: to complete msf_cross_docking module
2316
values = kwargs.get('values')
2317
assert values is not None, 'missing defaults'
2321
def do_create_picking(self, cr, uid, ids, context=None):
2323
create the picking ticket from selected stock moves
2327
assert context, 'context is not defined'
2328
assert 'partial_datas' in context, 'partial datas not present in context'
2329
partial_datas = context['partial_datas']
2332
move_obj = self.pool.get('stock.move')
2334
for pick in self.browse(cr, uid, ids, context=context):
2335
# create the new picking object
2336
# a sequence for each draft picking ticket is used for the picking ticket
2337
sequence = pick.sequence_id
2338
ticket_number = sequence.get_id(test='id', context=context)
2339
new_pick_id = self.copy(cr, uid, pick.id, {'name': (pick.name or 'NoName/000') + '-' + ticket_number,
2340
'backorder_id': pick.id,
2341
'move_lines': []}, context=dict(context, allow_copy=True,))
2342
# create stock moves corresponding to partial datas
2343
# for now, each new line from the wizard corresponds to a new stock.move
2344
# it could be interesting to regroup according to production lot/asset id
2345
move_ids = partial_datas[pick.id].keys()
2346
for move in move_obj.browse(cr, uid, move_ids, context=context):
2350
initial_qty = move.product_qty
2351
for partial in partial_datas[pick.id][move.id]:
2353
assert partial['product_id'] == move.product_id.id, 'product id is wrong, %s - %s'%(partial['product_id'], move.product_id.id)
2354
assert partial['product_uom'] == move.product_uom.id, 'product uom is wrong, %s - %s'%(partial['product_uom'], move.product_uom.id)
2356
count = count + partial['product_qty']
2357
# copy the stock move and set the quantity
2358
values = {'picking_id': new_pick_id,
2359
'product_qty': partial['product_qty'],
2360
'product_uos_qty': partial['product_qty'],
2361
'prodlot_id': partial['prodlot_id'],
2362
'asset_id': partial['asset_id'],
2363
'composition_list_id': partial['composition_list_id'],
2364
'backmove_id': move.id}
2366
values = self.do_create_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
2367
new_move = move_obj.copy(cr, uid, move.id, values, context=dict(context, keepLineNumber=True))
2369
# decrement the initial move, cannot be less than zero and mark the stock move as processed - will not be updated by delivery_mech anymore
2370
initial_qty = max(initial_qty - count, 0)
2371
move_obj.write(cr, uid, [move.id], {'product_qty': initial_qty, 'product_uos_qty': initial_qty, 'processed_stock_move': True}, context=context)
2373
# confirm the new picking ticket
2374
wf_service = netsvc.LocalService("workflow")
2375
wf_service.trg_validate(uid, 'stock.picking', new_pick_id, 'button_confirm', cr)
2376
# we force availability
2377
self.force_assign(cr, uid, [new_pick_id])
2379
# TODO which behavior
2380
#return {'type': 'ir.actions.act_window_close'}
2381
data_obj = self.pool.get('ir.model.data')
2382
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
2383
view_id = view_id and view_id[1] or False
2384
context.update({'picking_type': 'picking_ticket', 'picking_screen': True})
2385
return {'name':_("Picking Ticket"),
2386
'view_mode': 'form,tree',
2387
'view_id': [view_id],
2388
'view_type': 'form',
2389
'res_model': 'stock.picking',
2390
'res_id': new_pick_id,
2391
'type': 'ir.actions.act_window',
2396
def validate_picking(self, cr, uid, ids, context=None):
2398
validate the picking ticket
2400
# we need the context for the wizard switch
2405
name = _("Validate Picking Ticket")
2406
model = 'create.picking'
2408
wiz_obj = self.pool.get('wizard')
2409
# open the selected wizard
2410
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2412
def do_validate_picking_first_hook(self, cr, uid, ids, context, *args, **kwargs):
2414
hook to update new_move data. Originally: to complete msf_cross_docking module
2416
values = kwargs.get('values')
2417
assert values is not None, 'missing defaults'
2421
def do_validate_picking(self, cr, uid, ids, context=None):
2423
validate the picking ticket from selected stock moves
2425
move here the logic of validate picking
2426
available for picking loop
2430
assert context, 'context is not defined'
2431
assert 'partial_datas' in context, 'partial datas not present in context'
2432
partial_datas = context['partial_datas']
2435
date_tools = self.pool.get('date.tools')
2436
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
2437
today = time.strftime(db_date_format)
2440
move_obj = self.pool.get('stock.move')
2441
# create picking object
2442
create_picking_obj = self.pool.get('create.picking')
2445
for pick in self.browse(cr, uid, ids, context=context):
2446
# create stock moves corresponding to partial datas
2447
move_ids = partial_datas[pick.id].keys()
2448
for move in move_obj.browse(cr, uid, move_ids, context=context):
2451
# flag to update the first move - if split was performed during the validation, new stock moves are created
2454
initial_qty = move.product_qty
2455
for partial in partial_datas[pick.id][move.id]:
2457
assert partial['product_id'] == move.product_id.id, 'product id is wrong, %s - %s'%(partial['product_id'], move.product_id.id)
2458
assert partial['product_uom'] == move.product_uom.id, 'product uom is wrong, %s - %s'%(partial['product_uom'], move.product_uom.id)
2460
count = count + partial['product_qty']
2463
# update existing move
2464
values = {'product_qty': partial['product_qty'],
2465
'product_uos_qty': partial['product_qty'],
2466
'prodlot_id': partial['prodlot_id'],
2467
'composition_list_id': partial['composition_list_id'],
2468
'asset_id': partial['asset_id']}
2469
values = self.do_validate_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
2470
move_obj.write(cr, uid, [move.id], values, context=context)
2472
# split happend during the validation
2473
# copy the stock move and set the quantity
2474
values = {'state': 'assigned',
2475
'product_qty': partial['product_qty'],
2476
'product_uos_qty': partial['product_qty'],
2477
'prodlot_id': partial['prodlot_id'],
2478
'composition_list_id': partial['composition_list_id'],
2479
'asset_id': partial['asset_id']}
2480
values = self.do_validate_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
2481
new_move = move_obj.copy(cr, uid, move.id, values, context=dict(context, keepLineNumber=True))
2482
# decrement the initial move, cannot be less than zero
2483
diff_qty = initial_qty - count
2484
# the quantity after the validation does not correspond to the picking ticket quantity
2485
# the difference is written back to draft picking ticket
2486
# is positive if some qty was removed during the validation -> draft qty is increased
2487
# is negative if some qty was added during the validation -> draft qty is decreased
2489
# original move from the draft picking ticket which will be updated
2490
original_move = move.backmove_id
2491
backorder_qty = move_obj.read(cr, uid, [original_move.id], ['product_qty'], context=context)[0]['product_qty']
2492
backorder_qty = max(backorder_qty + diff_qty, 0)
2493
move_obj.write(cr, uid, [original_move.id], {'product_qty': backorder_qty}, context=context)
2495
# create the new ppl object
2496
ppl_number = pick.name.split("/")[1]
2497
# we want the copy to keep the production lot reference from picking ticket to pre-packing list
2498
new_ppl_id = self.copy(cr, uid, pick.id, {'name': 'PPL/' + ppl_number,
2500
'previous_step_id': pick.id,
2501
'backorder_id': False}, context=dict(context, keep_prodlot=True, allow_copy=True, keepLineNumber=True))
2502
new_ppl = self.browse(cr, uid, new_ppl_id, context=context)
2503
# update locations of stock moves - if the move quantity is equal to zero, the stock move is removed
2504
for move in new_ppl.move_lines:
2505
if move.product_qty:
2506
move_obj.write(cr, uid, [move.id], {'initial_location': move.location_id.id,
2507
'location_id': move.location_dest_id.id,
2508
'location_dest_id': new_ppl.warehouse_id.lot_dispatch_id.id,
2510
'date_expected': today,}, context=context)
2512
move_obj.unlink(cr, uid, [move.id], context=dict(context, skipResequencing=True))
2514
wf_service = netsvc.LocalService("workflow")
2515
wf_service.trg_validate(uid, 'stock.picking', new_ppl_id, 'button_confirm', cr)
2516
# simulate check assign button, as stock move must be available
2517
self.force_assign(cr, uid, [new_ppl_id])
2518
# trigger standard workflow for validated picking ticket
2519
self.action_move(cr, uid, [pick.id])
2520
wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
2522
# if the flow type is in quick mode, we perform the ppl steps automatically
2523
if pick.flow_type == 'quick':
2524
create_picking_obj.quick_mode(cr, uid, new_ppl, context=context)
2526
# TODO which behavior
2527
#return {'type': 'ir.actions.act_window_close'}
2528
data_obj = self.pool.get('ir.model.data')
2529
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_form')
2530
view_id = view_id and view_id[1] or False
2531
context.update({'picking_type': 'picking_ticket', 'ppl_screen': True})
2532
return {'name':_("Pre-Packing List"),
2533
'view_mode': 'form,tree',
2534
'view_id': [view_id],
2535
'view_type': 'form',
2536
'res_model': 'stock.picking',
2537
'res_id': new_ppl and new_ppl.id or False,
2538
'type': 'ir.actions.act_window',
2543
def ppl(self, cr, uid, ids, context=None):
2545
pack the ppl - open the ppl step1 wizard
2547
# we need the context for the wizard switch
2552
name = _("PPL Information - step1")
2553
model = 'create.picking'
2555
wiz_obj = self.pool.get('wizard')
2556
# open the selected wizard
2557
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2559
def do_ppl1(self, cr, uid, ids, context=None):
2561
- receives generated data from ppl in context
2562
- call action to ppl2 step with partial_datas_ppl1 in context
2563
- ids are the picking ids
2565
# we need the context for the wizard switch
2566
assert context, 'No context defined'
2569
name = _("PPL Information - step2")
2570
model = 'create.picking'
2572
wiz_obj = self.pool.get('wizard')
2573
# open the selected wizard
2574
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2576
def do_ppl2(self, cr, uid, ids, context=None):
2578
finalize the ppl logic
2581
assert context, 'context not defined'
2582
assert 'partial_datas_ppl1' in context, 'partial_datas_ppl1 no defined in context'
2584
move_obj = self.pool.get('stock.move')
2585
wf_service = netsvc.LocalService("workflow")
2587
partial_datas_ppl = context['partial_datas_ppl1']
2588
# picking ids from ids must be equal to picking ids from partial datas
2589
assert set(ids) == set(partial_datas_ppl.keys()), 'picking ids from ids and partial do not match'
2591
# update existing stock moves - create new one if split occurred
2593
for pick in self.browse(cr, uid, ids, context=context):
2594
# integrity check on move_ids - moves ids from picking and partial must be the same
2595
# dont take into account done moves, which represents returned products
2596
from_pick = [move.id for move in pick.move_lines if move.state in ('confirmed', 'assigned')]
2598
# the list of updated stock.moves
2599
# if a stock.move is updated, the next time a new move is created
2602
# browse returns a list of browse object in the same order as from_partial
2603
browse_moves = move_obj.browse(cr, uid, from_pick, context=context)
2604
moves = dict(zip(from_pick, browse_moves))
2606
for from_pack in partial_datas_ppl[pick.id]:
2607
for to_pack in partial_datas_ppl[pick.id][from_pack]:
2608
for move in partial_datas_ppl[pick.id][from_pack][to_pack]:
2610
from_partial.append(move)
2611
for partial in partial_datas_ppl[pick.id][from_pack][to_pack][move]:
2612
# {'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}
2614
assert partial['product_id'] == moves[move].product_id.id
2615
assert partial['asset_id'] == moves[move].asset_id.id
2616
assert partial['composition_list_id'] == moves[move].composition_list_id.id
2617
assert partial['product_uom'] == moves[move].product_uom.id
2618
assert partial['prodlot_id'] == moves[move].prodlot_id.id
2619
# dictionary of new values, used for creation or update
2620
# - qty_per_pack is a function at stock move level
2621
fields = ['product_qty', 'from_pack', 'to_pack', 'pack_type', 'length', 'width', 'height', 'weight']
2622
values = dict(zip(fields, [partial["%s"%x] for x in fields]))
2625
# if already updated, we create a new stock.move
2626
updated[move]['partial_qty'] += partial['product_qty']
2627
# force state to 'assigned'
2628
values.update(state='assigned')
2629
# copy stock.move with new product_qty, qty_per_pack. from_pack, to_pack, pack_type, length, width, height, weight
2630
move_obj.copy(cr, uid, move, values, context=context)
2632
# update the existing stock move
2633
updated[move] = {'initial': moves[move].product_qty, 'partial_qty': partial['product_qty']}
2634
move_obj.write(cr, uid, [move], values, context=context)
2636
# integrity check - all moves are treated and no more
2637
assert set(from_pick) == set(from_partial), 'move_ids are not equal pick:%s - partial:%s'%(set(from_pick), set(from_partial))
2638
# quantities are right
2639
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)
2640
# copy to 'packing' stock.picking
2641
# draft shipment is automatically created or updated if a shipment already
2642
pack_number = pick.name.split("/")[1]
2643
new_packing_id = self.copy(cr, uid, pick.id, {'name': 'PACK/' + pack_number,
2644
'subtype': 'packing',
2645
'previous_step_id': pick.id,
2646
'backorder_id': False,
2647
'shipment_id': False}, context=dict(context, keep_prodlot=True, allow_copy=True,))
2649
self.write(cr, uid, [new_packing_id], {'origin': pick.origin}, context=context)
2650
# update locations of stock moves and state as the picking stay at 'draft' state.
2651
# if return move have been done in previous ppl step, we remove the corresponding copied move (criteria: qty_per_pack == 0)
2652
new_packing = self.browse(cr, uid, new_packing_id, context=context)
2653
for move in new_packing.move_lines:
2654
if move.qty_per_pack == 0:
2655
move_obj.unlink(cr, uid, [move.id], context=context)
2657
move.write({'state': 'assigned',
2658
'location_id': new_packing.warehouse_id.lot_dispatch_id.id,
2659
'location_dest_id': new_packing.warehouse_id.lot_distribution_id.id}, context=context)
2661
# trigger standard workflow
2662
self.action_move(cr, uid, [pick.id])
2663
wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
2665
# TODO which behavior
2666
#return {'type': 'ir.actions.act_window_close'}
2667
data_obj = self.pool.get('ir.model.data')
2668
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
2669
view_id = view_id and view_id[1] or False
2670
return {'name':_("Shipment"),
2671
'view_mode': 'form,tree',
2672
'view_id': [view_id],
2673
'view_type': 'form',
2674
'res_model': 'shipment',
2675
'res_id': new_packing.shipment_id.id,
2676
'type': 'ir.actions.act_window',
2681
def return_products(self, cr, uid, ids, context=None):
2683
open the return products wizard
2685
# we need the context
2690
name = _("Return Products")
2691
model = 'create.picking'
2692
step = 'returnproducts'
2693
wiz_obj = self.pool.get('wizard')
2694
# open the selected wizard
2695
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2697
def do_return_products(self, cr, uid, ids, context=None):
2700
- update the draft picking ticket
2701
- create the back move
2706
assert context, 'context not defined'
2707
assert 'partial_datas' in context, 'partial_datas no defined in context'
2708
partial_datas = context['partial_datas']
2710
move_obj = self.pool.get('stock.move')
2711
wf_service = netsvc.LocalService("workflow")
2713
draft_picking_id = False
2714
for picking in self.browse(cr, uid, ids, context=context):
2716
# corresponding draft picking ticket
2717
draft_picking_id = picking.previous_step_id.backorder_id.id
2719
for move in move_obj.browse(cr, uid, partial_datas[picking.id].keys(), context=context):
2720
# we browse the updated moves (return qty > 0 is checked during data generation)
2722
data = partial_datas[picking.id][move.id]
2725
return_qty = data['qty_to_return']
2726
# initial qty is decremented
2727
initial_qty = move.product_qty
2728
initial_qty = max(initial_qty - return_qty, 0)
2729
values = {'product_qty': initial_qty}
2732
# if all products are sent back to stock, the move state is cancel - done for now, ideologic question, wahouuu!
2733
#values.update({'state': 'cancel'})
2734
values.update({'state': 'done'})
2735
move_obj.write(cr, uid, [move.id], values, context=context)
2737
# create a back move with the quantity to return to the good location
2738
# the good location is stored in the 'initial_location' field
2739
move_obj.copy(cr, uid, move.id, {'product_qty': return_qty,
2740
'location_dest_id': move.initial_location.id,
2741
'state': 'done'}, context=dict(context, keepLineNumber=True))
2743
# increase the draft move with the move quantity
2744
draft_move_id = move.backmove_id.id
2745
draft_initial_qty = move_obj.read(cr, uid, [draft_move_id], ['product_qty'], context=context)[0]['product_qty']
2746
draft_initial_qty += return_qty
2747
move_obj.write(cr, uid, [draft_move_id], {'product_qty': draft_initial_qty}, context=context)
2749
# log the increase action - display the picking ticket view form
2750
# TODO refactoring needed
2751
obj_data = self.pool.get('ir.model.data')
2752
res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_form')[1]
2753
self.log(cr, uid, picking.id, _("Products from Pre-Packing List (%s) have been returned to stock.")%(picking.name,), context={'view_id': res,})
2754
res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
2755
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,})
2756
# if all moves are done or canceled, the ppl is canceled
2758
for move in picking.move_lines:
2759
if move.state in ('assigned'):
2763
# we dont want the back move (done) to be canceled - so we dont use the original cancel workflow state because
2764
# action_cancel() from stock_picking would be called, this would cancel the done stock_moves
2765
# instead we move to the new return_cancel workflow state which simply set the stock_picking state to 'cancel'
2766
# TODO THIS DOESNT WORK - still done state - replace with trigger for now
2767
#wf_service.trg_validate(uid, 'stock.picking', picking.id, 'return_cancel', cr)
2768
wf_service.trg_write(uid, 'stock.picking', picking.id, cr)
2770
# TODO which behavior
2771
#return {'type': 'ir.actions.act_window_close'}
2772
data_obj = self.pool.get('ir.model.data')
2773
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
2774
view_id = view_id and view_id[1] or False
2775
context.update({'picking_type': 'picking_ticket'})
2777
'name':_("Picking Ticket"),
2778
'view_mode': 'form,tree',
2779
'view_id': [view_id],
2780
'view_type': 'form',
2781
'res_model': 'stock.picking',
2782
'res_id': draft_picking_id ,
2783
'type': 'ir.actions.act_window',
2788
def action_cancel(self, cr, uid, ids, context=None):
2790
override cancel state action from the workflow
2792
- depending on the subtype and state of the stock.picking object
2793
the behavior will be different
2795
Cancel button is active for the picking object:
2796
- subtype: 'picking'
2797
Cancel button is active for the shipment object:
2798
- subtype: 'packing'
2800
state is not taken into account as picking is canceled before
2804
move_obj = self.pool.get('stock.move')
2805
obj_data = self.pool.get('ir.model.data')
2807
# check the state of the picking
2808
for picking in self.browse(cr, uid, ids, context=context):
2809
# if draft and shipment is in progress, we cannot cancel
2810
if picking.subtype == 'picking' and picking.state in ('draft',):
2811
if self.has_picking_ticket_in_progress(cr, uid, [picking.id], context=context)[picking.id]:
2812
raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try to cancel again.'))
2813
return super(stock_picking, self).action_cancel(cr, uid, ids, context=context)
2814
# if not draft or qty does not match, the shipping is already in progress
2815
if picking.subtype == 'picking' and picking.state in ('done',):
2816
raise osv.except_osv(_('Warning !'), _('The shipment process is completed and cannot be canceled!'))
2818
# first call to super method, so if some checks fail won't perform other actions anyway
2819
# call super - picking is canceled
2820
super(stock_picking, self).action_cancel(cr, uid, ids, context=context)
2822
for picking in self.browse(cr, uid, ids, context=context):
2824
if picking.subtype == 'picking':
2826
# get the draft picking
2827
draft_picking_id = picking.backorder_id.id
2829
# for each move from picking ticket - could be split moves
2830
for move in picking.move_lines:
2831
# find the corresponding move in draft
2832
draft_move = move.backmove_id
2833
# increase the draft move with the move quantity
2834
initial_qty = move_obj.read(cr, uid, [draft_move.id], ['product_qty'], context=context)[0]['product_qty']
2835
initial_qty += move.product_qty
2836
move_obj.write(cr, uid, [draft_move.id], {'product_qty': initial_qty}, context=context)
2837
# log the increase action
2838
# TODO refactoring needed
2839
obj_data = self.pool.get('ir.model.data')
2840
res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
2841
self.log(cr, uid, draft_picking_id, _("The corresponding Draft Picking Ticket (%s) has been updated.")%(picking.backorder_id.name,), context={'view_id': res,})
2843
if picking.subtype == 'packing':
2844
# for each packing we get the draft packing
2845
draft_packing_id = picking.backorder_id.id
2847
# for each move from the packing
2848
for move in picking.move_lines:
2849
# corresponding draft move from draft **packing** object
2850
draft_move_id = move.backmove_packing_id.id
2851
# check the to_pack of draft move
2852
# if equal to draft to_pack = move from_pack - 1 (as we always take the pack with the highest number available)
2853
# we can increase the qty and update draft to_pack
2854
# otherwise we copy the draft packing move with updated quantity and from/to
2855
# we always create a new move
2856
draft_read = move_obj.read(cr, uid, [draft_move_id], ['product_qty', 'to_pack'], context=context)[0]
2857
draft_to_pack = draft_read['to_pack']
2858
if draft_to_pack + 1 == move.from_pack and False: # DEACTIVATED
2860
draft_qty = draft_read['product_qty'] + move.product_qty
2861
# update the draft move
2862
move_obj.write(cr, uid, [draft_move_id], {'product_qty': draft_qty, 'to_pack': move.to_pack}, context=context)
2864
# copy draft move (to be sure not to miss original info) with move qty and from/to
2865
move_obj.copy(cr, uid, draft_move_id, {'product_qty': move.product_qty,
2866
'from_pack': move.from_pack,
2867
'to_pack': move.to_pack,
2868
'state': 'assigned'}, context=context)
2875
class wizard(osv.osv):
2877
class offering open_wizard method for wizard control
2881
def open_wizard(self, cr, uid, ids, name=False, model=False, step='default', type='create', context=None):
2883
WARNING : IDS CORRESPOND TO ***MAIN OBJECT IDS*** (picking for example) take care when calling the method from wizards
2884
return the newly created wizard's id
2885
name, model, step are mandatory only for type 'create'
2890
if type == 'create':
2891
assert name, 'type "create" and no name defined'
2892
assert model, 'type "create" and no model defined'
2893
assert step, 'type "create" and no step defined'
2894
# create the memory object - passing the picking id to it through context
2895
wizard_id = self.pool.get(model).create(
2896
cr, uid, {}, context=dict(context,
2900
back_model=context.get('model', False),
2901
back_wizard_ids=context.get('wizard_ids', False),
2902
back_wizard_name=context.get('wizard_name', False),
2903
back_step=context.get('step', False),
2906
elif type == 'back':
2907
# open the previous wizard
2908
assert context['back_wizard_ids'], 'no back_wizard_ids defined'
2909
wizard_id = context['back_wizard_ids'][0]
2910
assert context['back_wizard_name'], 'no back_wizard_name defined'
2911
name = context['back_wizard_name']
2912
assert context['back_model'], 'no back_model defined'
2913
model = context['back_model']
2914
assert context['back_step'], 'no back_step defined'
2915
step = context['back_step']
2917
elif type == 'update':
2918
# refresh the same wizard
2919
assert context['wizard_ids'], 'no wizard_ids defined'
2920
wizard_id = context['wizard_ids'][0]
2921
assert context['wizard_name'], 'no wizard_name defined'
2922
name = context['wizard_name']
2923
assert context['model'], 'no model defined'
2924
model = context['model']
2925
assert context['step'], 'no step defined'
2926
step = context['step']
2928
# call action to wizard view
2931
'view_mode': 'form',
2933
'view_type': 'form',
2935
'res_id': wizard_id,
2936
'type': 'ir.actions.act_window',
2940
'context': dict(context,
2942
wizard_ids=[wizard_id],
2945
back_model=context.get('model', False),
2946
back_wizard_ids=context.get('wizard_ids', False),
2947
back_wizard_name=context.get('wizard_name', False),
2948
back_step=context.get('step', False),
2955
class product_product(osv.osv):
2957
add a getter for keep cool notion
2959
_inherit = 'product.product'
2961
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
2963
get functional values
2966
for product in self.browse(cr, uid, ids, context=context):
2967
values = {'is_keep_cool': False,
2969
result[product.id] = values
2971
is_keep_cool = bool(product.heat_sensitive_item)# in ('*', '**', '***',)
2972
values['is_keep_cool'] = is_keep_cool
2976
_columns = {'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals',),
2977
'prodlot_ids': fields.one2many('stock.production.lot', 'product_id', string='Batch Numbers',),
2983
class stock_move(osv.osv):
2987
_inherit = 'stock.move'
2989
def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
2991
facade for product_available function from product (stock)
2993
# get the corresponding product ids
2995
for d in self.read(cr, uid, ids, ['product_id'], context):
2996
result[d['id']] = d['product_id'][0]
2998
# get the virtual stock identified by product ids
2999
virtual = self.pool.get('product.product')._product_available(cr, uid, result.values(), field_names, arg, context)
3001
# replace product ids by corresponding stock move id
3002
result = dict([id, virtual[result[id]]] for id in result.keys())
3005
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
3007
get functional values
3010
for move in self.browse(cr, uid, ids, context=context):
3011
values = {'qty_per_pack': 0.0,
3012
'total_amount': 0.0,
3014
'currency_id': False,
3016
'is_dangerous_good': False,
3017
'is_keep_cool': False,
3018
'is_narcotic': False,
3019
'sale_order_line_number': 0,
3021
result[move.id] = values
3022
# number of packs with from/to values (integer)
3023
if move.to_pack == 0:
3026
num_of_packs = move.to_pack - move.from_pack + 1
3027
values['num_of_packs'] = num_of_packs
3030
values['qty_per_pack'] = move.product_qty / num_of_packs
3032
values['qty_per_pack'] = 0
3033
# total amount (float)
3034
total_amount = move.sale_line_id and move.sale_line_id.price_unit * move.product_qty or 0.0
3035
values['total_amount'] = total_amount
3036
# amount for one pack
3038
amount = total_amount / num_of_packs
3041
values['amount'] = amount
3043
values['currency_id'] = move.sale_line_id and move.sale_line_id.currency_id and move.sale_line_id.currency_id.id or False
3045
values['is_dangerous_good'] = move.product_id and move.product_id.dangerous_goods or False
3046
# keep cool - if heat_sensitive_item is True
3047
values['is_keep_cool'] = bool(move.product_id and move.product_id.heat_sensitive_item or False)
3049
values['is_narcotic'] = move.product_id and move.product_id.narcotic or False
3050
# sale_order_line_number
3051
values['sale_order_line_number'] = move.sale_line_id and move.sale_line_id.line_number or 0
3055
def default_get(self, cr, uid, fields, context=None):
3057
Set default values according to type and subtype
3062
res = super(stock_move, self).default_get(cr, uid, fields, context=context)
3064
if 'warehouse_id' in context and context.get('warehouse_id'):
3065
warehouse_id = context.get('warehouse_id')
3067
warehouse_id = self.pool.get('stock.warehouse').search(cr, uid, [], context=context)[0]
3068
res.update({'location_output_id': self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_output_id.id})
3070
loc_virtual_ids = self.pool.get('stock.location').search(cr, uid, [('name', '=', 'Virtual Locations')])
3071
loc_virtual_id = len(loc_virtual_ids) > 0 and loc_virtual_ids[0] or False
3072
res.update({'location_virtual_id': loc_virtual_id})
3074
if 'type' in context and context.get('type', False) == 'out':
3075
loc_stock_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_stock_id.id
3076
res.update({'location_id': loc_stock_id})
3078
if 'subtype' in context and context.get('subtype', False) == 'picking':
3079
loc_packing_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_packing_id.id
3080
res.update({'location_dest_id': loc_packing_id})
3081
elif 'subtype' in context and context.get('subtype', False) == 'standard':
3082
loc_output_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_output_id.id
3083
res.update({'location_dest_id': loc_output_id})
3087
_columns = {'from_pack': fields.integer(string='From p.'),
3088
'to_pack': fields.integer(string='To p.'),
3089
'pack_type': fields.many2one('pack.type', string='Pack Type'),
3090
'length' : fields.float(digits=(16,2), string='Length [cm]'),
3091
'width' : fields.float(digits=(16,2), string='Width [cm]'),
3092
'height' : fields.float(digits=(16,2), string='Height [cm]'),
3093
'weight' : fields.float(digits=(16,2), string='Weight p.p [kg]'),
3094
#'pack_family_id': fields.many2one('pack.family', string='Pack Family'),
3095
'initial_location': fields.many2one('stock.location', string='Initial Picking Location'),
3096
# relation to the corresponding move from draft **picking** ticket object
3097
'backmove_id': fields.many2one('stock.move', string='Corresponding move of previous step'),
3098
# relation to the corresponding move from draft **packing** ticket object
3099
'backmove_packing_id': fields.many2one('stock.move', string='Corresponding move of previous step in draft packing'),
3101
'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')),
3102
'qty_per_pack': fields.function(_vals_get, method=True, type='float', string='Qty p.p', multi='get_vals',),
3103
'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals',),
3104
'amount': fields.function(_vals_get, method=True, type='float', string='Pack Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals',),
3105
'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals_X',), # old_multi get_vals
3106
'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
3107
'is_dangerous_good': fields.function(_vals_get, method=True, type='boolean', string='Dangerous Good', multi='get_vals',),
3108
'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals',),
3109
'is_narcotic': fields.function(_vals_get, method=True, type='boolean', string='Narcotic', multi='get_vals',),
3110
'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
3111
# Fields used for domain
3112
'location_virtual_id': fields.many2one('stock.location', string='Virtual location'),
3113
'location_output_id': fields.many2one('stock.location', string='Output location'),
3114
'invoice_line_id': fields.many2one('account.invoice.line', string='Invoice line'),
3117
def action_cancel(self, cr, uid, ids, context=None):
3119
Confirm or check the procurement order associated to the stock move
3121
res = super(stock_move, self).action_cancel(cr, uid, ids, context=context)
3123
wf_service = netsvc.LocalService("workflow")
3125
proc_obj = self.pool.get('procurement.order')
3126
proc_ids = proc_obj.search(cr, uid, [('move_id', 'in', ids)], context=context)
3127
for proc in proc_obj.browse(cr, uid, proc_ids, context=context):
3128
if proc.state == 'draft':
3129
wf_service.trg_validate(uid, 'procurement.order', proc.id, 'button_confirm', cr)
3131
wf_service.trg_validate(uid, 'procurement.order', proc.id, 'button_check', cr)
3138
class sale_order(osv.osv):
3140
re-override to modify behavior for outgoing workflow
3142
_inherit = 'sale.order'
3143
_name = 'sale.order'
3145
def _hook_ship_create_execute_specific_code_01(self, cr, uid, ids, context=None, *args, **kwargs):
3147
Please copy this to your module's method also.
3148
This hook belongs to the action_ship_create method from sale>sale.py
3150
- allow to execute specific code at position 01
3152
super(sale_order, self)._hook_ship_create_execute_specific_code_01(cr, uid, ids, context=context, *args, **kwargs)
3154
pick_obj = self.pool.get('stock.picking')
3156
wf_service = netsvc.LocalService("workflow")
3157
proc_id = kwargs['proc_id']
3158
order = kwargs['order']
3159
if order.procurement_request :
3160
proc = self.pool.get('procurement.order').browse(cr, uid, [proc_id], context=context)
3161
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
3163
wf_service.trg_validate(uid, 'stock.picking', [pick_id], 'button_confirm', cr)
3165
# We also do a first 'check availability': cancel then check
3166
pick_obj.cancel_assign(cr, uid, [pick_id], context)
3167
pick_obj.action_assign(cr, uid, [pick_id], context)
3171
def _hook_ship_create_execute_specific_code_02(self, cr, uid, ids, context=None, *args, **kwargs):
3173
Please copy this to your module's method also.
3174
This hook belongs to the action_ship_create method from sale>sale.py
3176
- allow to execute specific code at position 02
3178
# Some verifications
3181
if isinstance(ids, (int, long)):
3185
pick_obj = self.pool.get('stock.picking')
3186
move_obj = self.pool.get('stock.move')
3187
order = kwargs['order']
3188
move_id = kwargs['move_id']
3189
pick_id = move_obj.browse(cr, uid, [move_id], context=context)[0].picking_id.id
3191
if order.procurement_request:
3192
move_obj.action_confirm(cr, uid, [move_id], context=context)
3193
# we Validate the picking "Confirms picking directly from draft state."
3194
pick_obj.draft_force_assign(cr, uid , [pick_id], context)
3195
# We also do a first 'check availability': cancel then check
3196
pick_obj.cancel_assign(cr, uid, [pick_id], context)
3197
pick_obj.action_assign(cr, uid, [pick_id], context)
3199
return super(sale_order, self)._hook_ship_create_execute_specific_code_02(cr, uid, ids, context, *args, **kwargs)
3201
def _hook_ship_create_stock_move(self, cr, uid, ids, context=None, *args, **kwargs):
3203
Please copy this to your module's method also.
3204
This hook belongs to the action_ship_create method from sale>sale.py
3206
- allow to modify the data for stock move creation
3208
move_data = super(sale_order, self)._hook_ship_create_stock_move(cr, uid, ids, context=context, *args, **kwargs)
3209
order = kwargs['order']
3211
if self.read(cr, uid, ids, ['procurement_request'], context=context)[0]['procurement_request']\
3212
and self.read(cr, uid, ids, ['location_requestor_id'], context=context)[0]['location_requestor_id']:
3213
move_data['type'] = 'internal'
3214
move_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
3215
move_data['location_dest_id'] = self.read(cr, uid, ids, ['location_requestor_id'], context=context)[0]['location_requestor_id'][0]
3217
# first go to packing location (PICK/PACK/SHIP) or output location (Simple OUT)
3218
# according to the configuration
3219
# first go to packing location
3220
setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
3221
if setup.delivery_process == 'simple':
3222
move_data['location_dest_id'] = order.shop_id.warehouse_id.lot_output_id.id
3224
move_data['location_dest_id'] = order.shop_id.warehouse_id.lot_packing_id.id
3226
if self.pool.get('product.product').browse(cr, uid, move_data['product_id']).type == 'service_recep':
3227
move_data['location_id'] = self.pool.get('stock.location').get_cross_docking_location(cr, uid)
3229
if 'sale_line_id' in move_data and move_data['sale_line_id']:
3230
sale_line = self.pool.get('sale.order.line').browse(cr, uid, move_data['sale_line_id'], context=context)
3231
if sale_line.type == 'make_to_order':
3232
move_data['location_id'] = self.pool.get('stock.location').get_cross_docking_location(cr, uid)
3233
move_data['move_cross_docking_ok'] = True
3234
# Update the stock.picking
3235
self.pool.get('stock.picking').write(cr, uid, move_data['picking_id'], {'cross_docking_ok': True}, context=context)
3237
move_data['state'] = 'confirmed'
3240
def _hook_ship_create_stock_picking(self, cr, uid, ids, context=None, *args, **kwargs):
3242
Please copy this to your module's method also.
3243
This hook belongs to the action_ship_create method from sale>sale.py
3245
- allow to modify the data for stock picking creation
3247
setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
3249
picking_data = super(sale_order, self)._hook_ship_create_stock_picking(cr, uid, ids, context=context, *args, **kwargs)
3250
order = kwargs['order']
3252
picking_data['state'] = 'draft'
3253
if setup.delivery_process == 'simple':
3254
picking_data['subtype'] = 'standard'
3255
# use the name according to picking ticket sequence
3256
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
3258
picking_data['subtype'] = 'picking'
3259
# use the name according to picking ticket sequence
3260
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket')
3263
if self.read(cr, uid, ids, ['procurement_request'], context=context):
3264
procurement_request = self.read(cr, uid, ids, ['procurement_request'], context=context)[0]['procurement_request']
3265
if procurement_request:
3266
picking_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
3267
picking_data['type'] = 'internal'
3268
picking_data['subtype'] = 'standard'
3269
picking_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
3270
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.internal')
3272
picking_data['name'] = pick_name
3273
picking_data['flow_type'] = 'full'
3274
picking_data['backorder_id'] = False
3275
picking_data['warehouse_id'] = order.shop_id.warehouse_id.id
3279
def _hook_ship_create_execute_picking_workflow(self, cr, uid, ids, context=None, *args, **kwargs):
3281
Please copy this to your module's method also.
3282
This hook belongs to the action_ship_create method from sale>sale.py
3284
- allow to avoid the stock picking workflow execution
3285
- trigger the logging message for the created picking, as it stays in draft state and no call to action_confirm is performed
3286
for the moment within the msf_outgoing logic
3288
setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
3289
cond = super(sale_order, self)._hook_ship_create_execute_picking_workflow(cr, uid, ids, context=context, *args, **kwargs)
3291
# On Simple OUT configuration, the system should confirm the OUT and launch a first check availability
3292
if setup.delivery_process != 'simple':
3293
cond = cond and False
3295
# diplay creation message for draft picking ticket
3296
picking_id = kwargs['picking_id']
3297
picking_obj = self.pool.get('stock.picking')
3299
picking_obj.log_picking(cr, uid, [picking_id], context=context)
3300
# Launch a first check availability
3301
self.pool.get('stock.picking').action_assign(cr, uid, [picking_id], context=context)
3308
class procurement_order(osv.osv):
3310
procurement order workflow
3312
_inherit = 'procurement.order'
3314
def _hook_check_mts_on_message(self, cr, uid, context=None, *args, **kwargs):
3316
Please copy this to your module's method also.
3317
This hook belongs to the _check_make_to_stock_product method from procurement>procurement.py>procurement.order
3319
- allow to modify the message written back to procurement order
3321
message = super(procurement_order, self)._hook_check_mts_on_message(cr, uid, context=context, *args, **kwargs)
3322
procurement = kwargs['procurement']
3323
if procurement.move_id.picking_id.state == 'draft' and procurement.move_id.picking_id.subtype == 'picking':
3324
message = _("Shipment Process in Progress.")