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', 'done') 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, non_stock_noupdate=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=dict(context, non_stock_noupdate=True))
469
# find the corresponding move in draft in the draft **picking**
470
draft_move = move.backmove_id
471
# increase the draft move with the move quantity
472
draft_initial_qty = move_obj.read(cr, uid, [draft_move.id], ['product_qty'], context=context)[0]['product_qty']
473
draft_initial_qty += return_qty
474
move_obj.write(cr, uid, [draft_move.id], {'product_qty': draft_initial_qty}, context=context)
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'
594
dispatch_name = _('Dispatch')
597
for shipment_id in partial_datas:
599
for packing in pick_obj.browse(cr, uid, partial_datas[shipment_id].keys(), context=context):
600
# corresponding draft packing -> backorder
601
draft_packing_id = packing.backorder_id.id
602
# corresponding draft shipment (all packing for a shipment belong to the same draft_shipment)
603
draft_shipment_id = packing.backorder_id.shipment_id.id
605
for from_pack in partial_datas[shipment_id][packing.id]:
606
for to_pack in partial_datas[shipment_id][packing.id][from_pack]:
607
# partial datas for one sequence of one packing
608
# could have multiple data multiple products in the same pack family
609
datas = partial_datas[shipment_id][packing.id][from_pack][to_pack]
610
# the corresponding moves
611
move_ids = move_obj.search(cr, uid, [('picking_id', '=', packing.id),
612
('from_pack', '=', from_pack),
613
('to_pack', '=', to_pack)], context=context)
615
# compute the sequences to stay/to return to draft packing
616
stay, back_to_draft = self.compute_sequences(cr, uid, ids, context=context,
621
# we have the information concerning movements to update the packing and the draft packing
623
# update the packing object, we update the existing move
624
# if needed new moves are created
626
for move in move_obj.browse(cr, uid, move_ids, context=context):
628
updated[move.id] = {'initial': move.product_qty, 'partial_qty': 0}
629
# loop through stay sequences
631
# corresponding number of packs
632
selected_number = seq[1] - seq[0] + 1
634
new_qty = selected_number * move.qty_per_pack
635
# for both cases, we update the from/to and compute the corresponding quantity
636
# if the move has been updated already, we copy/update
637
values = {'from_pack': seq[0],
639
'product_qty': new_qty,
642
# the original move is never modified, but canceled
643
updated[move.id]['partial_qty'] += new_qty
644
new_move_id = move_obj.copy(cr, uid, move.id, values, context=context)
647
# if 'partial_qty' not in updated[move.id]:
648
# updated[move.id]['partial_qty'] = 0
650
# loop through back_to_draft sequences
651
for seq in back_to_draft:
652
# for each sequence we add the corresponding stock move to draft packing
653
# corresponding number of packs
654
selected_number = seq[1] - seq[0] + 1
656
new_qty = selected_number * move.qty_per_pack
658
location_dispatch = move.picking_id.warehouse_id.lot_dispatch_id.id
659
location_distrib = move.picking_id.warehouse_id.lot_distribution_id.id
660
dispatch_name = move.picking_id.warehouse_id.lot_dispatch_id.name
661
values = {'from_pack': seq[0],
663
'product_qty': new_qty,
664
'location_id': location_distrib,
665
'location_dest_id': location_dispatch,
668
# create a back move in the packing object
669
# distribution -> dispatch
670
new_back_move_id = move_obj.copy(cr, uid, move.id, values, context=dict(context, non_stock_noupdate=True))
671
updated[move.id]['partial_qty'] += new_qty
673
# create the draft move
674
# dispatch -> distribution
675
# picking_id = draft_picking
676
values.update(location_id=location_dispatch,
677
location_dest_id=location_distrib,
678
picking_id=draft_packing_id,
680
new_draft_move_id = move_obj.copy(cr, uid, move.id, values, context=dict(context, non_stock_noupdate=True))
682
# quantities are right - stay + return qty = original qty
683
assert all([round(updated[m]['initial'], 14) == round(updated[m]['partial_qty'], 14) for m in updated.keys()]), 'initial quantity is not equal to the sum of partial quantities (%s).'%(updated)
684
# if packs are returned corresponding move is canceled
685
# cancel move or 0 qty + done ?
686
#move_obj.action_cancel(cr, uid, [move.id], context=context)
687
move_obj.write(cr, uid, [move.id], {'product_qty': 0.0, 'state': 'done', 'from_pack': 0, 'to_pack': 0,}, context=context)
689
# log corresponding action
690
shipment_name = self.read(cr, uid, shipment_id, ['name'], context=context)['name']
691
self.log(cr, uid, shipment_id, _("Packs from the shipped Shipment (%s) have been returned to %s location.")%(shipment_name,dispatch_name))
692
self.log(cr, uid, draft_shipment_id, _("The corresponding Draft Shipment (%s) has been updated.")%(packing.backorder_id.shipment_id.name,))
694
# call complete_finished on the shipment object
695
# if everything is allright (all draft packing are finished) the shipment is done also
696
self.complete_finished(cr, uid, partial_datas.keys(), context=context)
698
# TODO which behavior
699
#return {'type': 'ir.actions.act_window_close'}
700
data_obj = self.pool.get('ir.model.data')
701
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
702
view_id = view_id and view_id[1] or False
704
'name':_("Shipment"),
705
'view_mode': 'form,tree',
706
'view_id': [view_id],
708
'res_model': 'shipment',
709
'res_id': draft_shipment_id,
710
'type': 'ir.actions.act_window',
714
return {'type': 'ir.actions.act_window_close'}
716
def action_cancel(self, cr, uid, ids, context=None):
718
cancel the shipment which is not yet shipped (packed state)
720
- for each related packing object
721
- trigger the cancel workflow signal
722
logic is performed in the action_cancel method of stock.picking
724
pick_obj = self.pool.get('stock.picking')
725
wf_service = netsvc.LocalService("workflow")
727
for shipment in self.browse(cr, uid, ids, context=context):
728
# shipment state should be 'packed'
729
assert shipment.state == 'packed', 'cannot ship a shipment which is not in correct state - packed - %s'%shipment.state
731
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id)], context=context)
732
# call cancel workflow on corresponding packing objects
733
for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
734
# we cancel each picking object - action_cancel is overriden at stock_picking level for stock_picking of subtype == 'packing'
735
wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_cancel', cr)
736
# log corresponding action
737
self.log(cr, uid, shipment.id, _("The Shipment (%s) has been canceled.")%(shipment.name,))
738
self.log(cr, uid, shipment.backshipment_id.id, _("The corresponding Draft Shipment (%s) has been updated.")%(shipment.backshipment_id.name,))
742
def ship(self, cr, uid, ids, context=None):
744
we ship the created shipment, the state of the shipment is changed, we do not use any wizard
745
- state of the shipment is updated to 'shipped'
747
- modify locations of moves for the new packing
748
- trigger the workflow button_confirm for the new packing
749
- trigger the workflow to terminate the initial packing
750
- update the draft_picking_id fields of pack_families
751
- update the shipment_date of the corresponding sale_order if not set yet
753
pick_obj = self.pool.get('stock.picking')
754
pf_obj = self.pool.get('pack.family')
755
so_obj = self.pool.get('sale.order')
757
date_tools = self.pool.get('date.tools')
758
db_datetime_format = date_tools.get_db_datetime_format(cr, uid, context=context)
760
for shipment in self.browse(cr, uid, ids, context=context):
761
# shipment state should be 'packed'
762
assert shipment.state == 'packed', 'cannot ship a shipment which is not in correct state - packed - %s'%shipment.state
763
# the state does not need to be updated - function
764
# update actual ship date (shipment_actual_date) to today + time
765
today = time.strftime(db_datetime_format)
766
shipment.write({'shipment_actual_date': today,})
767
# corresponding packing objects
768
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id)], context=context)
770
for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
771
assert packing.subtype == 'packing'
772
# update the packing object for the same reason
773
# - an integrity check at _get_vals level of shipment states that all packing linked to a shipment must have the same state
774
# we therefore modify it before the copy, otherwise new (assigned) and old (done) are linked to the same shipment
775
# -> integrity check has been removed
776
pick_obj.write(cr, uid, [packing.id], {'shipment_id': False,}, context=context)
778
new_packing_id = pick_obj.copy(cr, uid, packing.id, {'name': packing.name,
779
'first_shipment_packing_id': packing.id,
780
'shipment_id': shipment.id,}, context=dict(context, keep_prodlot=True, allow_copy=True,))
781
pick_obj.write(cr, uid, [new_packing_id], {'origin': packing.origin}, context=context)
782
new_packing = pick_obj.browse(cr, uid, new_packing_id, context=context)
783
# update the shipment_date of the corresponding sale order if the date is not set yet - with current date
784
if new_packing.sale_id and not new_packing.sale_id.shipment_date:
785
# get the date format
786
date_tools = self.pool.get('date.tools')
787
date_format = date_tools.get_date_format(cr, uid, context=context)
788
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
789
today = time.strftime(date_format)
790
today_db = time.strftime(db_date_format)
791
so_obj.write(cr, uid, [new_packing.sale_id.id], {'shipment_date': today_db,}, context=context)
792
so_obj.log(cr, uid, new_packing.sale_id.id, _("Shipment Date of the Field Order '%s' has been updated to %s.")%(new_packing.sale_id.name, today))
794
# update locations of stock moves
795
for move in new_packing.move_lines:
796
move.write({'location_id': new_packing.warehouse_id.lot_distribution_id.id,
797
'location_dest_id': new_packing.warehouse_id.lot_output_id.id}, context=context)
799
wf_service = netsvc.LocalService("workflow")
800
wf_service.trg_validate(uid, 'stock.picking', new_packing_id, 'button_confirm', cr)
801
# simulate check assign button, as stock move must be available
802
pick_obj.force_assign(cr, uid, [new_packing_id])
803
# trigger standard workflow
804
pick_obj.action_move(cr, uid, [packing.id])
805
wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_done', cr)
807
# log the ship action
808
self.log(cr, uid, shipment.id, _('The Shipment %s has been shipped.')%(shipment.name,))
810
# TODO which behavior
813
def complete_finished(self, cr, uid, ids, context=None):
815
- check all draft packing corresponding to this shipment
816
- check the stock moves (qty and from/to)
817
- check all corresponding packing are done or canceled (no ongoing shipment)
818
- if all packings are ok, the draft is validated
819
- if all draft packing are ok, the shipment state is done
821
pick_obj = self.pool.get('stock.picking')
822
wf_service = netsvc.LocalService("workflow")
824
for shipment_base in self.browse(cr, uid, ids, context=context):
825
# the shipment which will be treated
826
shipment = shipment_base
828
if shipment.state not in ('draft',):
829
# it's not a draft shipment, check all corresponding packing, trg.write them
830
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
831
for packing_id in packing_ids:
832
wf_service.trg_write(uid, 'stock.picking', packing_id, cr)
834
# this shipment is possibly finished, we now check the corresponding draft shipment
835
# this will possibly validate the draft shipment, if everything is finished and corresponding draft picking
836
shipment = shipment.backshipment_id
838
# draft packing for this shipment - some draft packing can already be done for this shipment, so we filter according to state
839
draft_packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '=', 'draft'),], context=context)
840
for draft_packing in pick_obj.browse(cr, uid, draft_packing_ids, context=context):
841
assert draft_packing.subtype == 'packing', 'draft packing which is not packing subtype - %s'%draft_packing.subtype
842
assert draft_packing.state == 'draft', 'draft packing which is not draft state - %s'%draft_packing.state
843
# we check if the corresponding draft packing can be moved to done.
844
# if all packing with backorder_id equal to draft are done or canceled
845
# and the quantity for each stock move (state != done) of the draft packing is equal to zero
847
# we first check the stock moves quantities of the draft packing
848
# we can have done moves when some packs are returned
850
for move in draft_packing.move_lines:
851
if move.state not in ('done',):
854
elif move.from_pack or move.to_pack:
855
# qty = 0, from/to pack should have been set to zero
856
assert False, 'stock moves with 0 quantity but part of pack family sequence'
858
# check if ongoing packing are present, if present, we do not validate the draft one, the shipping is not finished
860
linked_packing_ids = pick_obj.search(cr, uid, [('backorder_id', '=', draft_packing.id),
861
('state', 'not in', ['done', 'cancel'])], context=context)
862
if linked_packing_ids:
866
# trigger the workflow for draft_picking
867
# confirm the new picking ticket
868
wf_service.trg_validate(uid, 'stock.picking', draft_packing.id, 'button_confirm', cr)
869
# we force availability
870
pick_obj.force_assign(cr, uid, [draft_packing.id])
872
pick_obj.action_move(cr, uid, [draft_packing.id])
873
wf_service.trg_validate(uid, 'stock.picking', draft_packing.id, 'button_done', cr)
874
# ask for draft picking validation, depending on picking completion
875
# if picking ticket is not completed, the validation will not complete
876
draft_packing.previous_step_id.previous_step_id.backorder_id.validate(context=context)
878
# all draft packing are validated (done state) - the state of shipment is automatically updated -> function
881
def shipment_create_invoice(self, cr, uid, ids, context=None):
883
Create invoices for validated shipment
885
invoice_obj = self.pool.get('account.invoice')
886
line_obj = self.pool.get('account.invoice.line')
887
partner_obj = self.pool.get('res.partner')
888
distrib_obj = self.pool.get('analytic.distribution')
889
sale_line_obj = self.pool.get('sale.order.line')
890
sale_obj = self.pool.get('sale.order')
891
company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id
896
if isinstance(ids, (int, long)):
899
for shipment in self.browse(cr, uid, ids, context=context):
901
for pack in shipment.pack_family_memory_ids:
902
for move in pack.move_lines:
903
if move.state != 'cancel' and (not move.sale_line_id or move.sale_line_id.order_id.order_policy == 'picking'):
909
payment_term_id = False
910
partner = shipment.partner_id2
912
raise osv.except_osv(_('Error, no partner !'),
913
_('Please put a partner on the shipment if you want to generate invoice.'))
915
inv_type = 'out_invoice'
917
if inv_type in ('out_invoice', 'out_refund'):
918
account_id = partner.property_account_receivable.id
919
payment_term_id = partner.property_payment_term and partner.property_payment_term.id or False
921
account_id = partner.property_account_payable.id
923
addresses = partner_obj.address_get(cr, uid, [partner.id], ['contact', 'invoice'])
924
today = time.strftime('%Y-%m-%d',time.localtime())
927
'name': shipment.name,
928
'origin': shipment.name or '',
930
'account_id': account_id,
931
'partner_id': partner.id,
932
'address_invoice_id': addresses['invoice'],
933
'address_contact_id': addresses['contact'],
934
'payment_term': payment_term_id,
935
'fiscal_position': partner.property_account_position.id,
936
'date_invoice': context.get('date_inv',False) or today,
940
cur_id = shipment.pack_family_memory_ids[0].currency_id.id
942
invoice_vals['currency_id'] = cur_id
944
journal_type = 'sale'
945
# Disturb journal for invoice only on intermission partner type
946
if shipment.partner_id2.partner_type == 'intermission':
947
if not company.intermission_default_counterpart or not company.intermission_default_counterpart.id:
948
raise osv.except_osv(_('Error'), _('Please configure a default intermission account in Company configuration.'))
949
invoice_vals['is_intermission'] = True
950
invoice_vals['account_id'] = company.intermission_default_counterpart.id
951
journal_type = 'intermission'
952
journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', journal_type),
953
('is_current_instance', '=', True)])
955
raise osv.except_osv(_('Warning'), _('No %s journal found!') % (journal_type,))
956
invoice_vals['journal_id'] = journal_ids[0]
958
invoice_id = invoice_obj.create(cr, uid, invoice_vals,
961
# Change currency for the intermission invoice
962
if shipment.partner_id2.partner_type == 'intermission':
963
company_currency = company.currency_id and company.currency_id.id or False
964
if not company_currency:
965
raise osv.except_osv(_('Warning'), _('No company currency found!'))
966
wiz_account_change = self.pool.get('account.change.currency').create(cr, uid, {'currency_id': company_currency}, context=context)
967
self.pool.get('account.change.currency').change_currency(cr, uid, [wiz_account_change], context={'active_id': invoice_id})
969
# Link the invoice to the shipment
970
self.write(cr, uid, [shipment.id], {'invoice_id': invoice_id}, context=context)
972
# For each stock moves, create an invoice line
973
for pack in shipment.pack_family_memory_ids:
974
for move in pack.move_lines:
975
if move.state == 'cancel':
978
if move.sale_line_id and move.sale_line_id.order_id.order_policy != 'picking':
981
origin = move.picking_id.name or ''
982
if move.picking_id.origin:
983
origin += ':' + move.picking_id.origin
985
if inv_type in ('out_invoice', 'out_refund'):
986
account_id = move.product_id.product_tmpl_id.\
987
property_account_income.id
989
account_id = move.product_id.categ_id.\
990
property_account_income_categ.id
992
account_id = move.product_id.product_tmpl_id.\
993
property_account_expense.id
995
account_id = move.product_id.categ_id.\
996
property_account_expense_categ.id
998
# Compute unit price from FO line if the move is linked to
999
price_unit = move.product_id.list_price
1000
if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
1001
uom_id = move.product_id.uom_id.id
1002
uos_id = move.product_id.uos_id and move.product_id.uos_id.id or False
1003
price = move.sale_line_id.price_unit
1004
coeff = move.product_id.uos_coeff
1005
if uom_id != uos_id and coeff != 0:
1006
price_unit = price / coeff
1008
price_unit = move.sale_line_id.price_unit
1010
# Get discount from FO line
1012
if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
1013
discount = move.sale_line_id.discount
1015
# Get taxes from FO line
1016
taxes = move.product_id.taxes_id
1017
if move.sale_line_id and move.sale_line_id.product_id.id == move.product_id.id:
1018
taxes = [x.id for x in move.sale_line_id.tax_id]
1020
if shipment.partner_id2:
1021
tax_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, shipment.partner_id2.property_account_position, taxes)
1023
tax_ids = map(lambda x: x.id, taxes)
1026
if move.sale_line_id:
1027
sol_ana_dist_id = move.sale_line_id.analytic_distribution_id or move.sale_line_id.order_id.analytic_distribution_id
1029
distrib_id = distrib_obj.copy(cr, uid, sol_ana_dist_id.id, context=context)
1031
#set UoS if it's a sale and the picking doesn't have one
1032
uos_id = move.product_uos and move.product_uos.id or False
1033
if not uos_id and inv_type in ('out_invoice', 'out_refund'):
1034
uos_id = move.product_uom.id
1035
account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
1037
line_id = line_obj.create(cr, uid, {'name': move.name,
1039
'invoice_id': invoice_id,
1041
'product_id': move.product_id.id,
1042
'account_id': account_id,
1043
'price_unit': price_unit,
1044
'discount': discount,
1045
'quantity': move.product_qty or move.product_uos_qty,
1046
'invoice_line_tax_id': [(6, 0, tax_ids)],
1047
'analytic_distribution_id': distrib_id,
1050
self.pool.get('shipment').write(cr, uid, [shipment.id], {'invoice_id': invoice_id}, context=context)
1051
if move.sale_line_id:
1052
sale_obj.write(cr, uid, [move.sale_line_id.order_id.id], {'invoice_ids': [(4, invoice_id)],})
1053
sale_line_obj.write(cr, uid, [move.sale_line_id.id], {'invoiced': True,
1054
'invoice_lines': [(4, line_id)],})
1058
def validate(self, cr, uid, ids, context=None):
1060
validate the shipment
1062
change the state to Done for the corresponding packing
1063
- validate the workflow for all the packings
1065
pick_obj = self.pool.get('stock.picking')
1066
wf_service = netsvc.LocalService("workflow")
1068
for shipment in self.browse(cr, uid, ids, context=context):
1069
# validate should only be called on shipped shipments
1070
assert shipment.state in ('shipped',), 'shipment state is not shipped'
1071
# corresponding packing objects - only the distribution -> customer ones
1072
# we have to discard picking object with state done, because when we return from shipment
1073
# all object of a given picking object, he is set to Done and still belong to the same shipment_id
1074
# another possibility would be to unlink the picking object from the shipment, set shipment_id to False
1075
# but in this case the returned pack families would not be displayed anymore in the shipment
1076
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '!=', 'done'),], context=context)
1078
for packing in pick_obj.browse(cr, uid, packing_ids, context=context):
1079
assert packing.subtype == 'packing' and packing.state == 'assigned'
1080
# trigger standard workflow
1081
pick_obj.action_move(cr, uid, [packing.id])
1082
wf_service.trg_validate(uid, 'stock.picking', packing.id, 'button_done', cr)
1084
# Create automatically the invoice
1085
self.shipment_create_invoice(cr, uid, shipment.id, context=context)
1087
# log validate action
1088
self.log(cr, uid, shipment.id, _('The Shipment %s has been closed.')%(shipment.name,))
1090
result = self.complete_finished(cr, uid, ids, context=context)
1093
def set_delivered(self, cr, uid, ids, context=None):
1095
set the delivered flag
1098
pick_obj = self.pool.get('stock.picking')
1099
for shipment in self.browse(cr, uid, ids, context=context):
1100
# validate should only be called on shipped shipments
1101
assert shipment.state in ['done'], 'shipment state is not shipped'
1102
# gather the corresponding packing and trigger the corresponding function
1103
packing_ids = pick_obj.search(cr, uid, [('shipment_id', '=', shipment.id), ('state', '=', 'done')], context=context)
1104
# set delivered all packings
1105
pick_obj.set_delivered(cr, uid, packing_ids, context=context)
1112
class pack_family_memory(osv.osv_memory):
1114
dynamic memory object for pack families
1116
_name = 'pack.family.memory'
1118
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
1120
get functional values
1123
for pf_memory in self.browse(cr, uid, ids, context=context):
1124
values = {'move_lines': [],
1126
'location_id': False,
1127
'location_dest_id': False,
1128
'total_amount': 0.0,
1130
'currency_id': False,
1132
'total_weight': 0.0,
1133
'total_volume': 0.0,
1135
result[pf_memory.id] = values
1136
# pack family related fields
1137
if pf_memory.to_pack == 0:
1140
num_of_packs = pf_memory.to_pack - pf_memory.from_pack + 1
1141
values['num_of_packs'] = num_of_packs
1142
values['total_weight'] = pf_memory.weight * num_of_packs
1143
values['total_volume'] = (pf_memory.length * pf_memory.width * pf_memory.height * num_of_packs) / 1000.0
1145
# moves related fields
1146
for move in pf_memory.draft_packing_id.move_lines:
1147
if move.from_pack == pf_memory.from_pack:
1148
if move.to_pack == pf_memory.to_pack:
1149
# this move is in the good packing object and corresponds to this pack family
1150
# we add it to the stock move list
1151
values['move_lines'].append(move.id)
1152
values['state'] = move.state
1153
values['location_id'] = move.location_id.id
1154
values['location_dest_id'] = move.location_dest_id.id
1155
values['total_amount'] += move.total_amount
1156
values['amount'] += move.amount
1157
values['currency_id'] = move.currency_id and move.currency_id.id or False
1159
# when multiple moves are modified from/to values, the first one would raise an exception as the second one is not written yet
1161
#raise osv.except_osv(_('Error !'), _('Integrity check failed! Pack Family and Stock Moves from/to do not match.'))
1165
_columns = {'name': fields.char(string='Reference', size=1024),
1166
'shipment_id': fields.many2one('shipment', string='Shipment'),
1167
'draft_packing_id': fields.many2one('stock.picking', string="Draft Packing Ref"),
1168
'sale_order_id': fields.many2one('sale.order', string="Sale Order Ref"),
1169
'ppl_id': fields.many2one('stock.picking', string="PPL Ref"),
1170
'from_pack': fields.integer(string='From p.'),
1171
'to_pack': fields.integer(string='To p.'),
1172
'pack_type': fields.many2one('pack.type', string='Pack Type'),
1173
'length' : fields.float(digits=(16,2), string='Length [cm]'),
1174
'width' : fields.float(digits=(16,2), string='Width [cm]'),
1175
'height' : fields.float(digits=(16,2), string='Height [cm]'),
1176
'weight' : fields.float(digits=(16,2), string='Weight p.p [kg]'),
1178
'move_lines': fields.function(_vals_get, method=True, type='one2many', relation='stock.move', string='Stock Moves', multi='get_vals',),
1179
'state': fields.function(_vals_get, method=True, type='selection', selection=[('draft', 'Draft'),
1180
('assigned', 'Available'),
1181
('stock_return', 'Returned to Stock'),
1182
('ship_return', 'Returned from Shipment'),
1183
('cancel', 'Cancelled'),
1184
('done', 'Closed'),], string='State', multi='get_vals',),
1185
'location_id': fields.function(_vals_get, method=True, type='many2one', relation='stock.location', string='Src Loc.', multi='get_vals',),
1186
'location_dest_id': fields.function(_vals_get, method=True, type='many2one', relation='stock.location', string='Dest. Loc.', multi='get_vals',),
1187
'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', multi='get_vals',),
1188
'amount': fields.function(_vals_get, method=True, type='float', string='Pack Amount', multi='get_vals',),
1189
'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
1190
'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals',),
1191
'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals',),
1192
'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals',),
1193
'description_ppl': fields.char('Description', size=256 ),
1196
_defaults = {'shipment_id': False,
1197
'draft_packing_id': False,
1200
pack_family_memory()
1203
class shipment2(osv.osv):
1207
_inherit = 'shipment'
1209
def on_change_partner(self, cr, uid, ids, partner_id, address_id, context=None):
1211
Change the delivery address when the partner change.
1217
v.update({'address_id': False})
1219
d.update({'address_id': [('partner_id', '=', partner_id)]})
1223
addr = self.pool.get('res.partner.address').browse(cr, uid, address_id, context=context)
1225
if not address_id or addr.partner_id.id != partner_id:
1226
addr = self.pool.get('res.partner').address_get(cr, uid, partner_id, ['delivery', 'default'])
1227
if not addr.get('delivery'):
1228
addr = addr.get('default')
1230
addr = addr.get('delivery')
1232
v.update({'address_id': addr})
1238
def _vals_get_2(self, cr, uid, ids, fields, arg, context=None):
1240
get functional values
1242
picking_obj = self.pool.get('stock.picking')
1245
for shipment in self.browse(cr, uid, ids, context=context):
1246
values = {'pack_family_memory_ids':[],
1248
result[shipment.id] = values
1249
# look for all corresponding packing
1250
packing_ids = picking_obj.search(cr, uid, [('shipment_id', '=', shipment.id),], context=context)
1251
# get the corresponding data
1252
data = picking_obj.generate_data_from_picking_for_pack_family(cr, uid, packing_ids, context=context)
1253
# create a memory family
1254
created_ids = picking_obj.create_pack_families_memory_from_data(cr, uid, data, shipment.id, context=context)
1255
values['pack_family_memory_ids'].extend(created_ids)
1259
_columns = {'pack_family_memory_ids': fields.function(_vals_get_2, method=True, type='one2many', relation='pack.family.memory', string='Memory Families', multi='get_vals_2',),
1265
class ppl_customize_label(osv.osv):
1269
_name = 'ppl.customize.label'
1273
Load msf_outgoing_data.xml before self
1275
if hasattr(super(ppl_customize_label, self), 'init'):
1276
super(ppl_customize_label, self).init(cr)
1278
mod_obj = self.pool.get('ir.module.module')
1279
logging.getLogger('init').info('HOOK: module msf_outgoing: loading data/msf_outgoing_data.xml')
1280
pathname = path.join('msf_outgoing', 'data/msf_outgoing_data.xml')
1281
file = tools.file_open(pathname)
1282
tools.convert_xml_import(cr, 'msf_outgoing', file, {}, mode='init', noupdate=False)
1284
_columns = {'name': fields.char(string='Name', size=1024,),
1285
'notes': fields.text(string='Notes'),
1286
#'packing_list_reference': fields.boolean(string='Packing List Reference'),
1287
'pre_packing_list_reference': fields.boolean(string='Pre-Packing List Reference'),
1288
'destination_partner': fields.boolean(string='Destination Partner'),
1289
'destination_address': fields.boolean(string='Destination Address'),
1290
'requestor_order_reference': fields.boolean(string='Requestor Order Reference'),
1291
'weight': fields.boolean(string='Weight'),
1292
#'shipment_reference': fields.boolean(string='Shipment Reference'),
1293
'packing_parcel_number': fields.boolean(string='Packing Parcel Number'),
1294
#'expedition_parcel_number': fields.boolean(string='Expedition Parcel Number'),
1295
'specific_information': fields.boolean(string='Specific Information'),
1296
'logo': fields.boolean(string='Company Logo'),
1299
_defaults = {'name': 'My Customization',
1301
#'packing_list_reference': True,
1302
'pre_packing_list_reference': True,
1303
'destination_partner': True,
1304
'destination_address': True,
1305
'requestor_order_reference': True,
1307
#'shipment_reference': True,
1308
'packing_parcel_number': True,
1309
#'expedition_parcel_number': True,
1310
'specific_information': True,
1314
ppl_customize_label()
1317
class stock_picking(osv.osv):
1319
override stock picking to add new attributes
1320
- flow_type: the type of flow (full, quick)
1321
- subtype: the subtype of picking object (picking, ppl, packing)
1322
- previous_step_id: the id of picking object of the previous step, picking for ppl, ppl for packing
1324
_inherit = 'stock.picking'
1325
_name = 'stock.picking'
1327
def fields_view_get(self, cr, uid, view_id, view_type, context=None, toolbar=False, submenu=False):
1329
Set the appropriate search view according to the context
1334
if not view_id and context.get('wh_dashboard') and view_type == 'search':
1336
if context.get('pick_type') == 'incoming':
1337
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'view_picking_in_search')[1]
1338
elif context.get('pick_type') == 'delivery':
1339
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'view_picking_out_search')[1]
1340
elif context.get('pick_type') == 'picking_ticket':
1341
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_search')[1]
1342
elif context.get('pick_type') == 'pack':
1343
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_search')[1]
1347
return super(stock_picking, self).fields_view_get(cr, uid, view_id, view_type, context=context, toolbar=toolbar, submenu=submenu)
1349
def unlink(self, cr, uid, ids, context=None):
1351
unlink test for draft
1353
datas = self.read(cr, uid, ids, ['state','type','subtype'], context=context)
1354
if [data for data in datas if data['state'] != 'draft']:
1355
raise osv.except_osv(_('Warning !'), _('Only draft picking tickets can be deleted.'))
1356
ids_picking_draft = [data['id'] for data in datas if data['subtype'] == 'picking' and data['type'] == 'out' and data['state'] == 'draft']
1357
if ids_picking_draft:
1358
data = self.has_picking_ticket_in_progress(cr, uid, ids, context=context)
1359
if [x for x in data.values() if x]:
1360
raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try again.'))
1362
return super(stock_picking, self).unlink(cr, uid, ids, context=context)
1364
def _hook_picking_get_view(self, cr, uid, ids, context=None, *args, **kwargs):
1365
pick = kwargs['pick']
1366
obj_data = self.pool.get('ir.model.data')
1367
view_list = {'standard': ('stock', 'view_picking_out_form'),
1368
'picking': ('msf_outgoing', 'view_picking_ticket_form'),
1369
'ppl': ('msf_outgoing', 'view_ppl_form'),
1370
'packing': ('msf_outgoing', 'view_packing_form'),
1372
if pick.type == 'out':
1373
context.update({'picking_type': pick.subtype == 'standard' and 'delivery_order' or 'picking_ticket'})
1374
module, view = view_list.get(pick.subtype,('msf_outgoing', 'view_picking_ticket_form'))
1376
return obj_data.get_object_reference(cr, uid, module, view)
1377
except ValueError, e:
1379
elif pick.type == 'in':
1380
context.update({'picking_type': 'incoming_shipment'})
1382
context.update({'picking_type': 'internal_move'})
1384
return super(stock_picking, self)._hook_picking_get_view(cr, uid, ids, context=context, *args, **kwargs)
1386
def _hook_custom_log(self, cr, uid, ids, context=None, *args, **kwargs):
1388
hook from stock>stock.py>log_picking
1389
update the domain and other values if necessary in the log creation
1391
result = super(stock_picking, self)._hook_custom_log(cr, uid, ids, context=context, *args, **kwargs)
1392
pick_obj = self.pool.get('stock.picking')
1393
pick = kwargs['pick']
1394
message = kwargs['message']
1395
if pick.type and pick.subtype:
1396
domain = [('type', '=', pick.type), ('subtype', '=', pick.subtype)]
1397
return self.pool.get('res.log').create(cr, uid,
1399
'res_model': pick_obj._name,
1406
def _hook_log_picking_log_cond(self, cr, uid, ids, context=None, *args, **kwargs):
1408
hook from stock>stock.py>stock_picking>log_picking
1409
specify if we display a log or not
1411
result = super(stock_picking, self)._hook_log_picking_log_cond(cr, uid, ids, context=context, *args, **kwargs)
1412
pick = kwargs['pick']
1413
if pick.subtype == 'packing':
1415
# if false the log will be defined by the method _hook_custom_log (which include a domain)
1416
if pick.type and pick.subtype:
1421
def copy(self, cr, uid, id, default=None, context=None):
1423
set the name corresponding to object subtype
1429
obj = self.browse(cr, uid, id, context=context)
1430
if not context.get('allow_copy', False):
1431
if obj.subtype == 'picking':
1432
if not obj.backorder_id:
1434
default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket'),
1436
date=date.today().strftime('%Y-%m-%d'),
1440
# if the corresponding draft picking ticket is done, we do not allow copy
1441
if obj.backorder_id and obj.backorder_id.state == 'done':
1442
raise osv.except_osv(_('Error !'), _('Corresponding Draft picking ticket is Closed. This picking ticket cannot be copied.'))
1443
# picking ticket, use draft sequence, keep other fields
1445
base = base.split('-')[0] + '-'
1446
default.update(name=base + obj.backorder_id.sequence_id.get_id(test='id', context=context),
1447
date=date.today().strftime('%Y-%m-%d'),
1450
elif obj.subtype == 'ppl':
1451
raise osv.except_osv(_('Error !'), _('Pre-Packing List copy is forbidden.'))
1452
# ppl, use the draft picking ticket sequence
1453
# if obj.previous_step_id and obj.previous_step_id.backorder_id:
1455
# base = base.split('-')[0] + '-'
1456
# default.update(name=base + obj.previous_step_id.backorder_id.sequence_id.get_id(test='id', context=context))
1458
# default.update(name=self.pool.get('ir.sequence').get(cr, uid, 'ppl'))
1460
result = super(stock_picking, self).copy(cr, uid, id, default=default, context=context)
1461
if not context.get('allow_copy', False):
1462
if obj.subtype == 'picking' and obj.backorder_id:
1463
# confirm the new picking ticket - the picking ticket should not stay in draft state !
1464
wf_service = netsvc.LocalService("workflow")
1465
wf_service.trg_validate(uid, 'stock.picking', result, 'button_confirm', cr)
1466
# we force availability
1467
self.force_assign(cr, uid, [result])
1470
def copy_data(self, cr, uid, id, default=None, context=None):
1472
reset one2many fields
1478
# reset one2many fields
1479
default.update(backorder_ids=[])
1480
default.update(previous_step_ids=[])
1481
default.update(pack_family_memory_ids=[])
1482
# the tag 'from_button' was added in the web client (openerp/controllers/form.py in the method duplicate) on purpose
1483
if context.get('from_button'):
1484
default.update(purchase_id=False)
1485
context['not_workflow'] = True
1486
result = super(stock_picking, self).copy_data(cr, uid, id, default=default, context=context)
1490
def _erase_prodlot_hook(self, cr, uid, id, context=None, *args, **kwargs):
1492
hook to keep the production lot when a stock move is copied
1494
res = super(stock_picking, self)._erase_prodlot_hook(cr, uid, id, context=context, *args, **kwargs)
1496
return res and not context.get('keep_prodlot', False)
1498
def has_picking_ticket_in_progress(self, cr, uid, ids, context=None):
1500
ids is the list of draft picking object we want to test
1501
completed means, we recursively check that next_step link object is cancel or done
1503
return true if picking tickets are in progress, meaning picking ticket or ppl or shipment not done exist
1507
if isinstance(ids, (int, long)):
1510
for obj in self.browse(cr, uid, ids, context=context):
1511
# by default, nothing is in progress
1513
# treat only draft picking
1514
assert obj.subtype in 'picking' and obj.state == 'draft', 'the validate function should only be called on draft picking ticket objects'
1515
for picking in obj.backorder_ids:
1516
# take care, is_completed returns a dictionary
1517
if not picking.is_completed()[picking.id]:
1523
def validate(self, cr, uid, ids, context=None):
1525
validate or not the draft picking ticket
1528
move_obj = self.pool.get('stock.move')
1530
for draft_picking in self.browse(cr, uid, ids, context=context):
1531
# the validate function should only be called on draft picking ticket
1532
assert draft_picking.subtype == 'picking' and draft_picking.state == 'draft', 'the validate function should only be called on draft picking ticket objects'
1533
#check the qty of all stock moves
1535
move_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_picking.id),
1536
('product_qty', '!=', 0.0),
1537
('state', 'not in', ['done', 'cancel'])], context=context)
1542
# then all child picking must be fully completed, meaning:
1543
# - all picking must be 'completed'
1544
# completed means, we recursively check that next_step link object is cancel or done
1545
if self.has_picking_ticket_in_progress(cr, uid, [draft_picking.id], context=context)[draft_picking.id]:
1549
# - all picking are completed (means ppl completed and all shipment validated)
1550
wf_service = netsvc.LocalService("workflow")
1551
wf_service.trg_validate(uid, 'stock.picking', draft_picking.id, 'button_confirm', cr)
1552
# we force availability
1553
draft_picking.force_assign()
1555
draft_picking.action_move()
1556
wf_service.trg_validate(uid, 'stock.picking', draft_picking.id, 'button_done', cr)
1560
def _vals_get_2(self, cr, uid, ids, fields, arg, context=None):
1562
get functional values
1565
for stock_picking in self.browse(cr, uid, ids, context=context):
1566
values = {'pack_family_memory_ids':[],
1568
result[stock_picking.id] = values
1570
# get the corresponding data for pack family memory
1571
data = self.generate_data_from_picking_for_pack_family(cr, uid, [stock_picking.id], context=context)
1572
# create a memory family - no shipment id
1573
created_ids = self.create_pack_families_memory_from_data(cr, uid, data, shipment_id=False, context=context)
1574
values['pack_family_memory_ids'].extend(created_ids)
1578
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
1580
get functional values
1583
for stock_picking in self.browse(cr, uid, ids, context=context):
1584
values = {'total_amount': 0.0,
1585
'currency_id': False,
1586
'is_dangerous_good': False,
1587
'is_keep_cool': False,
1588
'is_narcotic': False,
1590
'total_volume': 0.0,
1591
'total_weight': 0.0,
1592
#'is_completed': False,
1595
result[stock_picking.id] = values
1597
for family in stock_picking.pack_family_memory_ids:
1598
# number of packs from pack_family
1599
num_of_packs = family.num_of_packs
1600
values['num_of_packs'] += int(num_of_packs)
1602
total_weight = family.total_weight
1603
values['total_weight'] += total_weight
1604
total_volume = family.total_volume
1605
values['total_volume'] += total_volume
1607
for move in stock_picking.move_lines:
1608
# total amount (float)
1609
total_amount = move.total_amount
1610
values['total_amount'] = total_amount
1612
values['currency_id'] = move.currency_id and move.currency_id.id or False
1614
values['is_dangerous_good'] = move.is_dangerous_good
1615
# keep cool - if heat_sensitive_item is True
1616
values['is_keep_cool'] = move.is_keep_cool
1618
values['is_narcotic'] = move.is_narcotic
1619
# overall qty of products in all corresponding stock moves
1620
values['overall_qty'] += move.product_qty
1622
# completed field - based on the previous_step_ids field, recursive call from picking to draft packing and packing
1623
# - picking checks that the corresponding ppl is completed
1624
# - ppl checks that the corresponding draft packing and packings are completed
1625
# the recursion stops there because packing does not have previous_step_ids values
1626
# completed = stock_picking.state in ('done', 'cancel')
1628
# for next_step in stock_picking.previous_step_ids:
1629
# if not next_step.is_completed:
1633
# values['is_completed'] = completed
1637
def is_completed(self, cr, uid, ids, context=None):
1639
recursive test of completion
1640
- to be applied on picking ticket
1643
for picking in draft_picking.backorder_ids:
1644
# take care, is_completed returns a dictionary
1645
if not picking.is_completed()[picking.id]:
1648
***BEWARE: RETURNS A DICTIONARY !
1651
for stock_picking in self.browse(cr, uid, ids, context=context):
1653
state = stock_picking.state
1654
subtype = stock_picking.subtype
1655
completed = stock_picking.state in ('done', 'cancel')
1656
result[stock_picking.id] = completed
1658
for next_step in stock_picking.previous_step_ids:
1659
if not next_step.is_completed()[next_step.id]:
1661
result[stock_picking.id] = completed
1668
Load msf_outgoing_data.xml before self
1670
if hasattr(super(stock_picking, self), 'init'):
1671
super(stock_picking, self).init(cr)
1673
mod_obj = self.pool.get('ir.module.module')
1675
mod_id = mod_obj.search(cr, 1, [('name', '=', 'msf_outgoing'),])
1677
demo = mod_obj.read(cr, 1, mod_id, ['demo'])[0]['demo']
1680
logging.getLogger('init').info('HOOK: module msf_outgoing: loading data/msf_outgoing_data.xml')
1681
pathname = path.join('msf_outgoing', 'data/msf_outgoing_data.xml')
1682
file = tools.file_open(pathname)
1683
tools.convert_xml_import(cr, 'msf_outgoing', file, {}, mode='init', noupdate=False)
1685
def _qty_search(self, cr, uid, obj, name, args, context=None):
1686
""" Searches Ids of stock picking
1687
@return: Ids of locations
1692
stock_pickings = self.pool.get('stock.picking').search(cr, uid, [], context=context)
1695
for stock_picking in self.browse(cr, uid, stock_pickings, context=context):
1696
result[stock_picking.id] = 0.0
1697
for move in stock_picking.move_lines:
1698
result[stock_picking.id] += move.product_qty
1699
# construct the request
1700
# adapt the operator
1704
ids = [('id', 'in', [x for x in result.keys() if eval("%s %s %s"%(result[x], op, args[0][2]))])]
1707
def _get_picking_ids(self, cr, uid, ids, context=None):
1709
ids represents the ids of stock.move objects for which values have changed
1710
return the list of ids of picking object which need to get their state field updated
1712
self is stock.move object
1715
for obj in self.browse(cr, uid, ids, context=context):
1716
if obj.picking_id and obj.picking_id.id not in result:
1717
result.append(obj.picking_id.id)
1720
def _get_draft_moves(self, cr, uid, ids, field_name, args, context=None):
1722
Returns True if there is draft moves on Picking Ticket
1726
for pick in self.browse(cr, uid, ids, context=context):
1727
res[pick.id] = False
1728
for move in pick.move_lines:
1729
if move.state == 'draft':
1735
_columns = {'flow_type': fields.selection([('full', 'Full'),('quick', 'Quick')], readonly=True, states={'draft': [('readonly', False),],}, string='Flow Type'),
1736
'subtype': fields.selection([('standard', 'Standard'), ('picking', 'Picking'),('ppl', 'PPL'),('packing', 'Packing')], string='Subtype'),
1737
'backorder_ids': fields.one2many('stock.picking', 'backorder_id', string='Backorder ids',),
1738
'previous_step_id': fields.many2one('stock.picking', 'Previous step'),
1739
'previous_step_ids': fields.one2many('stock.picking', 'previous_step_id', string='Previous Step ids',),
1740
'shipment_id': fields.many2one('shipment', string='Shipment'),
1741
'sequence_id': fields.many2one('ir.sequence', 'Picking Ticket Sequence', help="This field contains the information related to the numbering of the picking tickets.", ondelete='cascade'),
1742
'first_shipment_packing_id': fields.many2one('stock.picking', 'Shipment First Step'),
1743
#'pack_family_ids': fields.one2many('pack.family', 'ppl_id', string='Pack Families',),
1744
# attributes for specific packing labels
1745
'ppl_customize_label': fields.many2one('ppl.customize.label', string='Labels Customization',),
1746
# warehouse info (locations) are gathered from here - allow shipment process without sale order
1747
'warehouse_id': fields.many2one('stock.warehouse', string='Warehouse', required=True,),
1748
# flag for converted picking
1749
'converted_to_standard': fields.boolean(string='Converted to Standard'),
1751
'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals_X'), # old_multi get_vals
1752
'total_volume': fields.function(_vals_get, method=True, type='float', string=u'Total Volume[dm³]', multi='get_vals'),
1753
'total_weight': fields.function(_vals_get, method=True, type='float', string='Total Weight[kg]', multi='get_vals'),
1754
'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals'),
1755
'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals'),
1756
'is_dangerous_good': fields.function(_vals_get, method=True, type='boolean', string='Dangerous Good', multi='get_vals'),
1757
'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals'),
1758
'is_narcotic': fields.function(_vals_get, method=True, type='boolean', string='Narcotic', multi='get_vals'),
1759
'overall_qty': fields.function(_vals_get, method=True, fnct_search=_qty_search, type='float', string='Overall Qty', multi='get_vals',
1760
store= {'stock.move': (_get_picking_ids, ['product_qty', 'picking_id'], 10),}),
1761
#'is_completed': fields.function(_vals_get, method=True, type='boolean', string='Completed Process', multi='get_vals',),
1762
'pack_family_memory_ids': fields.function(_vals_get_2, method=True, type='one2many', relation='pack.family.memory', string='Memory Families', multi='get_vals_2',),
1763
'description_ppl': fields.char('Description', size=256 ),
1764
'has_draft_moves': fields.function(_get_draft_moves, method=True, type='boolean', string='Has draft moves ?', store=False),
1766
_defaults = {'flow_type': 'full',
1767
'ppl_customize_label': lambda obj, cr, uid, c: len(obj.pool.get('ppl.customize.label').search(cr, uid, [('name', '=', 'Default Label'),], context=c)) and obj.pool.get('ppl.customize.label').search(cr, uid, [('name', '=', 'Default Label'),], context=c)[0] or False,
1768
'subtype': 'standard',
1769
'first_shipment_packing_id': False,
1770
'warehouse_id': lambda obj, cr, uid, c: len(obj.pool.get('stock.warehouse').search(cr, uid, [], context=c)) and obj.pool.get('stock.warehouse').search(cr, uid, [], context=c)[0] or False,
1771
'converted_to_standard': False,
1773
#_order = 'origin desc, name asc'
1774
_order = 'name desc'
1776
def onchange_move(self, cr, uid, ids, context=None):
1778
Display or not the 'Confirm' button on Picking Ticket
1780
res = super(stock_picking, self).onchange_move(cr, uid, ids, context=context)
1783
has_draft_moves = self._get_draft_moves(cr, uid, ids, 'has_draft_moves', False)[ids[0]]
1784
res.setdefault('value', {}).update({'has_draft_moves': has_draft_moves})
1788
def picking_ticket_data(self, cr, uid, ids, context=None):
1790
generate picking ticket data for report creation
1792
- sale order line without product: does not work presently
1794
- many sale order line with same product: stored in different dictionary with line id as key.
1795
so the same product could be displayed many times in the picking ticket according to sale order
1797
- many stock move with same product: two cases, if from different sale order lines, the above rule applies,
1798
if from the same order line, they will be stored according to prodlot id
1800
- many stock move with same prodlot (so same product): if same sale order line, the moves will be
1801
stored in the same structure, with global quantity, i.e. this batch for this product for this
1802
sale order line will be displayed only once with summed quantity from concerned stock moves
1804
[sale_line.id][product_id][prodlot_id]
1806
other prod lot, not used are added in order that all prod lot are displayed
1808
to check, if a move does not come from the sale order line:
1809
stored with line id False, product is relevant, multiple
1810
product for the same 0 line id is possible
1813
for stock_picking in self.browse(cr, uid, ids, context=context):
1815
result[stock_picking.id] = {'obj': stock_picking,
1818
for move in stock_picking.move_lines:
1819
if move.product_id: # product is mandatory at stock_move level ;)
1820
sale_line_id = move.sale_line_id and move.sale_line_id.id or False
1821
# structure, data is reorganized in order to regroup according to sale order line > product > production lot
1822
# and to sum the quantities corresponding to different levels because this is impossible within the rml framework
1824
.setdefault(sale_line_id, {}) \
1825
.setdefault('products', {}) \
1826
.setdefault(move.product_id.id, {}) \
1827
.setdefault('uoms', {}) \
1828
.setdefault(move.product_uom.id, {}) \
1829
.setdefault('lots', {})
1831
# ** sale order line info**
1832
values[sale_line_id]['obj'] = move.sale_line_id or False
1834
# **uom level info**
1835
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['obj'] = move.product_uom
1837
# **prodlot level info**
1839
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'].setdefault(move.prodlot_id.id, {})
1840
# qty corresponding to this production lot
1841
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id].setdefault('reserved_qty', 0)
1842
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id]['reserved_qty'] += move.product_qty
1843
# store the object for info retrieval
1844
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['lots'][move.prodlot_id.id]['obj'] = move.prodlot_id
1846
# **product level info**
1847
# total quantity from STOCK_MOVES for one sale order line (directly for one product)
1848
# or if not linked to a sale order line, stock move created manually, the line id is False
1849
# and in this case the product is important
1850
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id].setdefault('qty_to_pick_sm', 0)
1851
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['qty_to_pick_sm'] += move.product_qty
1852
# total quantity from SALE_ORDER_LINES, which can be different from the one from stock moves
1853
# if stock moves have been created manually in the picking, no present in the so, equal to 0 if not linked to an so
1854
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id].setdefault('qty_to_pick_so', 0)
1855
values[sale_line_id]['products'][move.product_id.id]['uoms'][move.product_uom.id]['qty_to_pick_so'] += move.sale_line_id and move.sale_line_id.product_uom_qty or 0.0
1856
# store the object for info retrieval
1857
values[sale_line_id]['products'][move.product_id.id]['obj'] = move.product_id
1859
# all moves have been treated
1860
# complete the lot lists for each product
1861
for sale_line in values.values():
1862
for product in sale_line['products'].values():
1863
for uom in product['uoms'].values():
1864
# loop through all existing production lot for this product - all are taken into account, internal and external
1865
for lot in product['obj'].prodlot_ids:
1866
if lot.id not in uom['lots'].keys():
1867
# the lot is not present, we add it
1868
uom['lots'][lot.id] = {}
1869
uom['lots'][lot.id]['obj'] = lot
1870
# reserved qty is 0 since no stock moves correspond to this lot
1871
uom['lots'][lot.id]['reserved_qty'] = 0.0
1875
def action_confirm_moves(self, cr, uid, ids, context=None):
1877
Confirm all stock moves of the picking
1879
for pick in self.browse(cr, uid, ids, context=context):
1880
for move in pick.move_lines:
1881
if move.state == 'draft':
1882
self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context)
1886
def create_sequence(self, cr, uid, vals, context=None):
1888
Create new entry sequence for every new picking
1889
@param cr: cursor to database
1890
@param user: id of current user
1891
@param ids: list of record ids to be process
1892
@param context: context arguments, like lang, time zone
1893
@return: return a result
1895
example of name: 'PICK/xxxxx'
1896
example of code: 'picking.xxxxx'
1897
example of prefix: 'PICK'
1898
example of padding: 5
1900
seq_pool = self.pool.get('ir.sequence')
1901
seq_typ_pool = self.pool.get('ir.sequence.type')
1903
default_name = 'Stock Picking'
1904
default_code = 'stock.picking'
1911
name = vals.get('name', False)
1914
code = vals.get('code', False)
1917
prefix = vals.get('prefix', False)
1919
prefix = default_prefix
1920
padding = vals.get('padding', False)
1922
padding = default_padding
1928
seq_typ_pool.create(cr, uid, types)
1936
return seq_pool.create(cr, uid, seq)
1938
def generate_data_from_picking_for_pack_family(self, cr, uid, pick_ids, object_type='shipment', from_pack=False, to_pack=False, context=None):
1940
generate the data structure from the stock.picking object
1942
we can limit the generation to certain from/to sequence
1944
one data for each move_id - here is the difference with data generated from partial
1947
{pick_id: {from_pack: {to_pack: {move_id: {data}}}}}
1949
if the move has a quantity equal to 0, it means that no pack are available,
1950
these moves are therefore not taken into account for the pack families generation
1952
TODO: integrity constraints
1954
Note: why the same dictionary is repeated n times for n moves, because
1955
it is directly used when we create the pack families. could be refactored
1956
with one dic per from/to with a 'move_ids' entry
1958
assert bool(from_pack) == bool(to_pack), 'from_pack and to_pack must be either both filled or empty'
1962
if object_type == 'shipment':
1963
# all moves are taken into account, therefore the back moves are represented
1964
# by done pack families
1966
elif object_type == 'memory':
1967
# done moves are not displayed as pf as we cannot select these packs anymore (they are returned)
1970
assert False, 'Should not reach this line'
1972
for pick in self.browse(cr, uid, pick_ids, context=context):
1973
result[pick.id] = {}
1974
for move in pick.move_lines:
1975
if not from_pack or move.from_pack == from_pack:
1976
if not to_pack or move.to_pack == to_pack:
1977
# the quantity must be positive and the state depends on the window's type
1978
if move.product_qty and move.state not in states:
1979
# subtype == ppl - called from stock picking
1980
if pick.subtype == 'ppl':
1982
.setdefault(move.from_pack, {}) \
1983
.setdefault(move.to_pack, {})[move.id] = {'sale_order_id': pick.sale_id.id,
1984
'ppl_id': pick.id, # only change between ppl - packing
1985
'from_pack': move.from_pack,
1986
'to_pack': move.to_pack,
1987
'pack_type': move.pack_type.id,
1988
'length': move.length,
1989
'width': move.width,
1990
'height': move.height,
1991
'weight': move.weight,
1992
'draft_packing_id': pick.id,
1993
'description_ppl': pick.description_ppl,
1995
# subtype == packing - caled from shipment
1996
elif pick.subtype == 'packing':
1998
.setdefault(move.from_pack, {}) \
1999
.setdefault(move.to_pack, {})[move.id] = {'sale_order_id': pick.sale_id.id,
2000
'ppl_id': pick.previous_step_id.id,
2001
'from_pack': move.from_pack,
2002
'to_pack': move.to_pack,
2003
'pack_type': move.pack_type.id,
2004
'length': move.length,
2005
'width': move.width,
2006
'height': move.height,
2007
'weight': move.weight,
2008
'draft_packing_id': pick.id,
2011
if object_type != 'memory':
2012
result[pick.id][move.from_pack][move.to_pack][move.id]['description_ppl'] = pick.description_ppl
2016
def create_pack_families_memory_from_data(self, cr, uid, data, shipment_id, context=None,):
2018
- clear existing pack family memory objects is not necessary thanks to vaccum system
2019
-> in fact cleaning old memory objects reslults in a bug, because when we click on a
2020
pf memory to see it's form view, the shipment view is regenerated (delete the pf)
2021
but then the form view uses the old pf memory id for data and it crashes. Cleaning
2022
of previous pf memory has therefore been removed.
2023
- generate new ones based on data
2024
- return created ids
2026
pf_memory_obj = self.pool.get('pack.family.memory')
2027
# find and delete existing objects
2028
#ids = pf_memory_obj.search(cr, uid, [('shipment_id', '=', shipment_id),], context=context)
2029
#pf_memory_obj.unlink(cr, uid, ids, context=context)
2031
# create pack family memory
2032
for picking in data.values():
2033
for from_pack in picking.values():
2034
for to_pack in from_pack.values():
2035
for move_id in to_pack.keys():
2036
move_data = to_pack[move_id]
2037
# create corresponding memory object
2038
move_data.update(name='_name',
2039
shipment_id=shipment_id,)
2040
id = pf_memory_obj.create(cr, uid, move_data, context=context)
2041
created_ids.append(id)
2044
def create(self, cr, uid, vals, context=None):
2046
creation of a stock.picking of subtype 'packing' triggers
2048
- creation of corresponding shipment
2050
# For picking ticket from scratch, invoice it !
2051
if not vals.get('sale_id') and not vals.get('purchase_id') and not vals.get('invoice_state') and 'type' in vals and vals['type'] == 'out':
2052
vals['invoice_state'] = '2binvoiced'
2054
date_tools = self.pool.get('date.tools')
2055
fields_tools = self.pool.get('fields.tools')
2056
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
2057
db_datetime_format = date_tools.get_db_datetime_format(cr, uid, context=context)
2061
# the action adds subtype in the context depending from which screen it is created
2062
if context.get('picking_screen', False) and not vals.get('name', False):
2063
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket')
2064
vals.update(subtype='picking',
2070
if context.get('ppl_screen', False) and not vals.get('name', False):
2071
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'ppl')
2072
vals.update(subtype='ppl',
2078
shipment_obj = self.pool.get('shipment')
2080
move_obj = self.pool.get('stock.move')
2084
if 'subtype' in vals and vals['subtype'] == 'picking':
2085
# creation of a new picking ticket
2086
assert 'backorder_id' in vals, 'No backorder_id'
2088
if not vals['backorder_id']:
2089
# creation of *draft* picking ticket
2090
vals.update(sequence_id=self.create_sequence(cr, uid, {'name':vals['name'],
2091
'code':vals['name'],
2093
'padding':2}, context=context))
2095
if 'subtype' in vals and vals['subtype'] == 'packing':
2096
# creation of a new packing
2097
assert 'backorder_id' in vals, 'No backorder_id'
2098
assert 'shipment_id' in vals, 'No shipment_id'
2100
if not vals['backorder_id']:
2101
# creation of *draft* picking ticket
2102
vals.update(sequence_id=self.create_sequence(cr, uid, {'name':vals['name'],
2103
'code':vals['name'],
2106
}, context=context))
2108
# create packing object
2109
new_packing_id = super(stock_picking, self).create(cr, uid, vals, context=context)
2111
if 'subtype' in vals and vals['subtype'] == 'packing':
2112
# creation of a new packing
2113
assert 'backorder_id' in vals, 'No backorder_id'
2114
assert 'shipment_id' in vals, 'No shipment_id'
2116
if vals['backorder_id'] and vals['shipment_id']:
2117
# ship of existing shipment
2120
return new_packing_id
2122
if vals['backorder_id'] and not vals['shipment_id']:
2123
# data from do_create_shipment method
2124
assert 'partial_datas_shipment' in context, 'Missing partial_datas_shipment'
2125
assert 'draft_shipment_id' in context, 'Missing draft_shipment_id'
2126
assert 'draft_packing_id' in context, 'Missing draft_packing_id'
2127
assert 'shipment_id' in context, 'Missing shipment_id'
2128
draft_shipment_id = context['draft_shipment_id']
2129
draft_packing_id = context['draft_packing_id']
2130
data = context['partial_datas_shipment'][draft_shipment_id][draft_packing_id]
2131
shipment_id = context['shipment_id']
2132
# We have a backorder_id, no shipment_id
2133
# -> we have just created a shipment
2134
# the created packing object has no stock_move
2135
# - we create the sock move from the data in context
2136
# - if no shipment in context, create a new shipment object
2137
# - generate the data from the new picking object
2138
# - create the pack families
2139
for from_pack in data:
2140
for to_pack in data[from_pack]:
2141
# total number of packs
2142
total_num = to_pack - from_pack + 1
2143
# number of selected packs to ship
2144
# note: when the data is generated, lines without selected_number are not kept, so we have nothing to check here
2145
selected_number = data[from_pack][to_pack][0]['selected_number']
2146
# we take the packs with the highest numbers
2148
selected_from_pack = to_pack - selected_number + 1
2149
selected_to_pack = to_pack
2150
# update initial moves
2151
if selected_number == total_num:
2152
# if all packs have been selected, from/to are set to 0
2153
initial_from_pack = 0
2156
initial_from_pack = from_pack
2157
initial_to_pack = to_pack - selected_number
2159
# find the corresponding moves
2160
moves_ids = move_obj.search(cr, uid, [('picking_id', '=', draft_packing_id),
2161
('from_pack', '=', from_pack),
2162
('to_pack', '=', to_pack),], context=context)
2164
for move in move_obj.browse(cr, uid, moves_ids, context=context):
2165
# we compute the selected quantity
2166
selected_qty = move.qty_per_pack * selected_number
2167
# create the new move - store the back move from draft **packing** object
2168
new_move = move_obj.copy(cr, uid, move.id, {'picking_id': new_packing_id,
2169
'product_qty': selected_qty,
2170
'from_pack': selected_from_pack,
2171
'to_pack': selected_to_pack,
2172
'backmove_packing_id': move.id,}, context=context)
2174
# update corresponding initial move
2175
initial_qty = move.product_qty
2176
initial_qty = max(initial_qty - selected_qty, 0)
2177
# if all packs have been selected, from/to have been set to 0
2178
# update the original move object - the corresponding original shipment (draft)
2179
# is automatically updated generically in the write method
2180
move_obj.write(cr, uid, [move.id], {'product_qty': initial_qty,
2181
'from_pack': initial_from_pack,
2182
'to_pack': initial_to_pack}, context=context)
2184
if not vals['backorder_id']:
2185
# creation of packing after ppl validation
2186
# find an existing shipment or create one - depends on new pick state
2187
shipment_ids = shipment_obj.search(cr, uid, [('state', '=', 'draft'), ('address_id', '=', vals['address_id'])], context=context)
2188
# only one 'draft' shipment should be available
2189
assert len(shipment_ids) in (0, 1), 'Only one draft shipment should be available for a given address at a time - %s'%len(shipment_ids)
2190
# get rts of corresponding sale order
2191
sale_id = self.read(cr, uid, [new_packing_id], ['sale_id'], context=context)
2192
sale_id = sale_id[0]['sale_id']
2194
sale_id = sale_id[0]
2196
today = time.strftime(db_datetime_format)
2197
rts = self.pool.get('sale.order').read(cr, uid, [sale_id], ['ready_to_ship_date'], context=context)[0]['ready_to_ship_date']
2199
rts = date.today().strftime(db_date_format)
2201
shipment_lt = fields_tools.get_field_from_company(cr, uid, object=self._name, field='shipment_lead_time', context=context)
2202
rts_obj = datetime.strptime(rts, db_date_format)
2203
rts = rts_obj + relativedelta(days=shipment_lt or 0)
2204
rts = rts.strftime(db_date_format)
2206
if not len(shipment_ids):
2207
# no shipment, create one - no need to specify the state, it's a function
2208
name = self.pool.get('ir.sequence').get(cr, uid, 'shipment')
2209
addr = self.pool.get('res.partner.address').browse(cr, uid, vals['address_id'], context=context)
2210
partner_id = addr.partner_id and addr.partner_id.id or False
2211
values = {'name': name,
2212
'address_id': vals['address_id'],
2213
'partner_id2': partner_id,
2214
'shipment_expected_date': rts,
2215
'shipment_actual_date': rts,
2216
'sequence_id': self.create_sequence(cr, uid, {'name':name,
2219
'padding':2}, context=context)}
2221
shipment_id = shipment_obj.create(cr, uid, values, context=context)
2222
shipment_obj.log(cr, uid, shipment_id, _('The new Draft Shipment %s has been created.')%(name,))
2224
shipment_id = shipment_ids[0]
2225
shipment = shipment_obj.browse(cr, uid, shipment_id, context=context)
2226
# if expected ship date of shipment is greater than rts, update shipment_expected_date and shipment_actual_date
2227
shipment_expected = datetime.strptime(shipment.shipment_expected_date, db_datetime_format)
2228
if rts_obj < shipment_expected:
2229
shipment.write({'shipment_expected_date': rts, 'shipment_actual_date': rts,}, context=context)
2230
shipment_name = shipment.name
2231
shipment_obj.log(cr, uid, shipment_id, _('The ppl has been added to the existing Draft Shipment %s.')%(shipment_name,))
2233
# update the new pick with shipment_id
2234
self.write(cr, uid, [new_packing_id], {'shipment_id': shipment_id}, context=context)
2236
return new_packing_id
2238
def _hook_action_assign_raise_exception(self, cr, uid, ids, context=None, *args, **kwargs):
2240
Please copy this to your module's method also.
2241
This hook belongs to the action_assign method from stock>stock.py>stock_picking class
2243
- allow to choose wether or not an exception should be raised in case of no stock move
2245
res = super(stock_picking, self)._hook_action_assign_raise_exception(cr, uid, ids, context=context, *args, **kwargs)
2246
return res and False
2248
def _hook_log_picking_modify_message(self, cr, uid, ids, context=None, *args, **kwargs):
2250
stock>stock.py>log_picking
2251
update the message to be displayed by the function
2253
pick = kwargs['pick']
2254
message = kwargs['message']
2255
# if the picking is converted to standard, and state is confirmed
2256
if pick.converted_to_standard and pick.state == 'confirmed':
2257
return 'The Preparation Picking has been converted to simple Out. ' + message
2258
if pick.type == 'out' and pick.subtype == 'picking':
2259
kwargs['message'] = message.replace('Delivery Order', 'Picking Ticket')
2260
elif pick.type == 'out' and pick.subtype == 'packing':
2261
kwargs['message'] = message.replace('Delivery Order', 'Packing List')
2262
elif pick.type == 'out' and pick.subtype == 'ppl':
2263
kwargs['message'] = message.replace('Delivery Order', 'Pre-Packing List')
2264
return super(stock_picking, self)._hook_log_picking_modify_message(cr, uid, ids, context, *args, **kwargs)
2266
def convert_to_standard(self, cr, uid, ids, context=None):
2268
check of back orders exists, if not, convert to standard: change subtype to standard, and trigger workflow
2270
only one picking object at a time
2275
date_tools = self.pool.get('date.tools')
2276
fields_tools = self.pool.get('fields.tools')
2277
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
2278
for obj in self.browse(cr, uid, ids, context=context):
2279
# the convert function should only be called on draft picking ticket
2280
assert obj.subtype == 'picking' and obj.state == 'draft', 'the convert function should only be called on draft picking ticket objects'
2281
if self.has_picking_ticket_in_progress(cr, uid, [obj.id], context=context)[obj.id]:
2282
raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try again.'))
2284
# log a message concerning the conversion
2285
new_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
2286
self.log(cr, uid, obj.id, _('The Preparation Picking (%s) has been converted to simple Out (%s).')%(obj.name, new_name))
2287
# change subtype and name
2288
obj.write({'name': new_name,
2289
'subtype': 'standard',
2290
'converted_to_standard': True,
2292
# all destination location of the stock moves must be output location of warehouse - lot_output_id
2293
# if corresponding sale order, date and date_expected are updated to rts + shipment lt
2294
for move in obj.move_lines:
2295
# was previously set to confirmed/assigned, otherwise, when we confirm the stock picking,
2296
# using draft_force_assign, the moves are not treated because not in draft
2297
# and the corresponding chain location on location_dest_id was not computed
2298
# we therefore set them back in draft state before treatment
2299
if move.product_qty == 0.0:
2300
vals = {'state': 'done'}
2302
vals = {'state': 'draft'}
2303
# If the move comes from a DPO, don't change the destination location
2305
vals.update({'location_dest_id': obj.warehouse_id.lot_output_id.id})
2309
shipment_lt = fields_tools.get_field_from_company(cr, uid, object=self._name, field='shipment_lead_time', context=context)
2310
rts = datetime.strptime(obj.sale_id.ready_to_ship_date, db_date_format)
2311
rts = rts + relativedelta(days=shipment_lt or 0)
2312
rts = rts.strftime(db_date_format)
2313
vals.update({'date': rts, 'date_expected': rts, 'state': 'draft'})
2314
move.write(vals, context=context)
2315
if move.product_qty == 0.00:
2316
move.action_done(context=context)
2319
# trigger workflow (confirm picking)
2320
self.draft_force_assign(cr, uid, [obj.id])
2321
# check availability
2322
self.action_assign(cr, uid, [obj.id], context=context)
2324
# TODO which behavior
2325
data_obj = self.pool.get('ir.model.data')
2326
view_id = data_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
2327
view_id = view_id and view_id[1] or False
2328
context.update({'picking_type': 'delivery_order'})
2329
return {'name':_("Delivery Orders"),
2330
'view_mode': 'form,tree',
2331
'view_id': [view_id],
2332
'view_type': 'form',
2333
'res_model': 'stock.picking',
2335
'type': 'ir.actions.act_window',
2340
def create_picking(self, cr, uid, ids, context=None):
2342
open the wizard to create (partial) picking tickets
2344
# we need the context for the wizard switch
2349
name = _("Create Picking Ticket")
2350
model = 'create.picking'
2352
wiz_obj = self.pool.get('wizard')
2353
# open the selected wizard
2354
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2356
def do_create_picking_first_hook(self, cr, uid, ids, context, *args, **kwargs):
2358
hook to update new_move data. Originally: to complete msf_cross_docking module
2360
values = kwargs.get('values')
2361
assert values is not None, 'missing defaults'
2365
def do_create_picking(self, cr, uid, ids, context=None):
2367
create the picking ticket from selected stock moves
2371
assert context, 'context is not defined'
2372
assert 'partial_datas' in context, 'partial datas not present in context'
2373
partial_datas = context['partial_datas']
2376
move_obj = self.pool.get('stock.move')
2378
for pick in self.browse(cr, uid, ids, context=context):
2379
# create the new picking object
2380
# a sequence for each draft picking ticket is used for the picking ticket
2381
sequence = pick.sequence_id
2382
ticket_number = sequence.get_id(test='id', context=context)
2383
new_pick_id = self.copy(cr, uid, pick.id, {'name': (pick.name or 'NoName/000') + '-' + ticket_number,
2384
'backorder_id': pick.id,
2385
'move_lines': []}, context=dict(context, allow_copy=True,))
2386
# create stock moves corresponding to partial datas
2387
# for now, each new line from the wizard corresponds to a new stock.move
2388
# it could be interesting to regroup according to production lot/asset id
2389
move_ids = partial_datas[pick.id].keys()
2390
for move in move_obj.browse(cr, uid, move_ids, context=context):
2394
initial_qty = move.product_qty
2395
for partial in partial_datas[pick.id][move.id]:
2397
assert partial['product_id'] == move.product_id.id, 'product id is wrong, %s - %s'%(partial['product_id'], move.product_id.id)
2398
assert partial['product_uom'] == move.product_uom.id, 'product uom is wrong, %s - %s'%(partial['product_uom'], move.product_uom.id)
2400
count = count + partial['product_qty']
2401
# copy the stock move and set the quantity
2402
values = {'picking_id': new_pick_id,
2403
'product_qty': partial['product_qty'],
2404
'product_uos_qty': partial['product_qty'],
2405
'prodlot_id': partial['prodlot_id'],
2406
'asset_id': partial['asset_id'],
2407
'composition_list_id': partial['composition_list_id'],
2408
'backmove_id': move.id}
2410
values = self.do_create_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
2411
new_move = move_obj.copy(cr, uid, move.id, values, context=dict(context, keepLineNumber=True))
2413
# decrement the initial move, cannot be less than zero and mark the stock move as processed - will not be updated by delivery_mech anymore
2414
initial_qty = max(initial_qty - count, 0)
2415
move_obj.write(cr, uid, [move.id], {'product_qty': initial_qty, 'product_uos_qty': initial_qty, 'processed_stock_move': True}, context=context)
2417
# confirm the new picking ticket
2418
wf_service = netsvc.LocalService("workflow")
2419
wf_service.trg_validate(uid, 'stock.picking', new_pick_id, 'button_confirm', cr)
2420
# we force availability
2421
self.force_assign(cr, uid, [new_pick_id])
2423
# TODO which behavior
2424
#return {'type': 'ir.actions.act_window_close'}
2425
data_obj = self.pool.get('ir.model.data')
2426
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
2427
view_id = view_id and view_id[1] or False
2428
context.update({'picking_type': 'picking_ticket', 'picking_screen': True})
2429
return {'name':_("Picking Ticket"),
2430
'view_mode': 'form,tree',
2431
'view_id': [view_id],
2432
'view_type': 'form',
2433
'res_model': 'stock.picking',
2434
'res_id': new_pick_id,
2435
'type': 'ir.actions.act_window',
2440
def validate_picking(self, cr, uid, ids, context=None):
2442
validate the picking ticket
2444
# we need the context for the wizard switch
2449
name = _("Validate Picking Ticket")
2450
model = 'create.picking'
2452
wiz_obj = self.pool.get('wizard')
2453
# open the selected wizard
2454
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2456
def do_validate_picking_first_hook(self, cr, uid, ids, context, *args, **kwargs):
2458
hook to update new_move data. Originally: to complete msf_cross_docking module
2460
values = kwargs.get('values')
2461
assert values is not None, 'missing defaults'
2465
def do_validate_picking(self, cr, uid, ids, context=None):
2467
validate the picking ticket from selected stock moves
2469
move here the logic of validate picking
2470
available for picking loop
2474
assert context, 'context is not defined'
2475
assert 'partial_datas' in context, 'partial datas not present in context'
2476
partial_datas = context['partial_datas']
2479
date_tools = self.pool.get('date.tools')
2480
db_date_format = date_tools.get_db_date_format(cr, uid, context=context)
2481
today = time.strftime(db_date_format)
2484
move_obj = self.pool.get('stock.move')
2485
# create picking object
2486
create_picking_obj = self.pool.get('create.picking')
2489
for pick in self.browse(cr, uid, ids, context=context):
2490
# create stock moves corresponding to partial datas
2491
move_ids = partial_datas[pick.id].keys()
2492
for move in move_obj.browse(cr, uid, move_ids, context=context):
2495
# flag to update the first move - if split was performed during the validation, new stock moves are created
2498
initial_qty = move.product_qty
2499
for partial in partial_datas[pick.id][move.id]:
2501
assert partial['product_id'] == move.product_id.id, 'product id is wrong, %s - %s'%(partial['product_id'], move.product_id.id)
2502
assert partial['product_uom'] == move.product_uom.id, 'product uom is wrong, %s - %s'%(partial['product_uom'], move.product_uom.id)
2504
count = count + partial['product_qty']
2507
# update existing move
2508
values = {'product_qty': partial['product_qty'],
2509
'product_uos_qty': partial['product_qty'],
2510
'prodlot_id': partial['prodlot_id'],
2511
'composition_list_id': partial['composition_list_id'],
2512
'asset_id': partial['asset_id']}
2513
values = self.do_validate_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
2514
move_obj.write(cr, uid, [move.id], values, context=context)
2516
# split happend during the validation
2517
# copy the stock move and set the quantity
2518
values = {'state': 'assigned',
2519
'product_qty': partial['product_qty'],
2520
'product_uos_qty': partial['product_qty'],
2521
'prodlot_id': partial['prodlot_id'],
2522
'composition_list_id': partial['composition_list_id'],
2523
'asset_id': partial['asset_id']}
2524
values = self.do_validate_picking_first_hook(cr, uid, ids, context=context, partial_datas=partial_datas, values=values, move=move)
2525
new_move = move_obj.copy(cr, uid, move.id, values, context=dict(context, keepLineNumber=True))
2526
# decrement the initial move, cannot be less than zero
2527
diff_qty = initial_qty - count
2528
# the quantity after the validation does not correspond to the picking ticket quantity
2529
# the difference is written back to draft picking ticket
2530
# is positive if some qty was removed during the validation -> draft qty is increased
2531
# is negative if some qty was added during the validation -> draft qty is decreased
2533
# original move from the draft picking ticket which will be updated
2534
original_move = move.backmove_id
2535
backorder_qty = move_obj.read(cr, uid, [original_move.id], ['product_qty'], context=context)[0]['product_qty']
2536
backorder_qty = max(backorder_qty + diff_qty, 0)
2537
move_obj.write(cr, uid, [original_move.id], {'product_qty': backorder_qty}, context=context)
2539
# create the new ppl object
2540
ppl_number = pick.name.split("/")[1]
2541
# we want the copy to keep the production lot reference from picking ticket to pre-packing list
2542
new_ppl_id = self.copy(cr, uid, pick.id, {'name': 'PPL/' + ppl_number,
2544
'previous_step_id': pick.id,
2545
'backorder_id': False}, context=dict(context, keep_prodlot=True, allow_copy=True, keepLineNumber=True))
2546
new_ppl = self.browse(cr, uid, new_ppl_id, context=context)
2547
# update locations of stock moves - if the move quantity is equal to zero, the stock move is removed
2548
for move in new_ppl.move_lines:
2549
if move.product_qty:
2550
move_obj.write(cr, uid, [move.id], {'initial_location': move.location_id.id,
2551
'location_id': move.location_dest_id.id,
2552
'location_dest_id': new_ppl.warehouse_id.lot_dispatch_id.id,
2554
'date_expected': today,}, context=context)
2556
move_obj.unlink(cr, uid, [move.id], context=dict(context, skipResequencing=True))
2558
wf_service = netsvc.LocalService("workflow")
2559
wf_service.trg_validate(uid, 'stock.picking', new_ppl_id, 'button_confirm', cr)
2560
# simulate check assign button, as stock move must be available
2561
self.force_assign(cr, uid, [new_ppl_id])
2562
# trigger standard workflow for validated picking ticket
2563
self.action_move(cr, uid, [pick.id])
2564
wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
2566
# if the flow type is in quick mode, we perform the ppl steps automatically
2567
if pick.flow_type == 'quick':
2568
create_picking_obj.quick_mode(cr, uid, new_ppl, context=context)
2570
# TODO which behavior
2571
#return {'type': 'ir.actions.act_window_close'}
2572
data_obj = self.pool.get('ir.model.data')
2573
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_form')
2574
view_id = view_id and view_id[1] or False
2575
context.update({'picking_type': 'picking_ticket', 'ppl_screen': True})
2576
return {'name':_("Pre-Packing List"),
2577
'view_mode': 'form,tree',
2578
'view_id': [view_id],
2579
'view_type': 'form',
2580
'res_model': 'stock.picking',
2581
'res_id': new_ppl and new_ppl.id or False,
2582
'type': 'ir.actions.act_window',
2587
def ppl(self, cr, uid, ids, context=None):
2589
pack the ppl - open the ppl step1 wizard
2591
# we need the context for the wizard switch
2596
name = _("PPL Information - step1")
2597
model = 'create.picking'
2599
wiz_obj = self.pool.get('wizard')
2600
# open the selected wizard
2601
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2603
def do_ppl1(self, cr, uid, ids, context=None):
2605
- receives generated data from ppl in context
2606
- call action to ppl2 step with partial_datas_ppl1 in context
2607
- ids are the picking ids
2609
# we need the context for the wizard switch
2610
assert context, 'No context defined'
2613
name = _("PPL Information - step2")
2614
model = 'create.picking'
2616
wiz_obj = self.pool.get('wizard')
2617
# open the selected wizard
2618
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2620
def do_ppl2(self, cr, uid, ids, context=None):
2622
finalize the ppl logic
2625
assert context, 'context not defined'
2626
assert 'partial_datas_ppl1' in context, 'partial_datas_ppl1 no defined in context'
2628
move_obj = self.pool.get('stock.move')
2629
wf_service = netsvc.LocalService("workflow")
2631
partial_datas_ppl = context['partial_datas_ppl1']
2632
# picking ids from ids must be equal to picking ids from partial datas
2633
assert set(ids) == set(partial_datas_ppl.keys()), 'picking ids from ids and partial do not match'
2635
# update existing stock moves - create new one if split occurred
2637
for pick in self.browse(cr, uid, ids, context=context):
2638
# integrity check on move_ids - moves ids from picking and partial must be the same
2639
# dont take into account done moves, which represents returned products
2640
from_pick = [move.id for move in pick.move_lines if move.state in ('confirmed', 'assigned')]
2642
# the list of updated stock.moves
2643
# if a stock.move is updated, the next time a new move is created
2646
# browse returns a list of browse object in the same order as from_partial
2647
browse_moves = move_obj.browse(cr, uid, from_pick, context=context)
2648
moves = dict(zip(from_pick, browse_moves))
2650
for from_pack in partial_datas_ppl[pick.id]:
2651
for to_pack in partial_datas_ppl[pick.id][from_pack]:
2652
for move in partial_datas_ppl[pick.id][from_pack][to_pack]:
2654
from_partial.append(move)
2655
for partial in partial_datas_ppl[pick.id][from_pack][to_pack][move]:
2656
# {'asset_id': False, 'weight': False, 'product_id': 77, 'product_uom': 1, 'pack_type': False, 'length': False, 'to_pack': 1, 'height': False, 'from_pack': 1, 'prodlot_id': False, 'qty_per_pack': 18.0, 'product_qty': 18.0, 'width': False, 'move_id': 179}
2658
assert partial['product_id'] == moves[move].product_id.id
2659
assert partial['asset_id'] == moves[move].asset_id.id
2660
assert partial['composition_list_id'] == moves[move].composition_list_id.id
2661
assert partial['product_uom'] == moves[move].product_uom.id
2662
assert partial['prodlot_id'] == moves[move].prodlot_id.id
2663
# dictionary of new values, used for creation or update
2664
# - qty_per_pack is a function at stock move level
2665
fields = ['product_qty', 'from_pack', 'to_pack', 'pack_type', 'length', 'width', 'height', 'weight']
2666
values = dict(zip(fields, [partial["%s"%x] for x in fields]))
2669
# if already updated, we create a new stock.move
2670
updated[move]['partial_qty'] += partial['product_qty']
2671
# force state to 'assigned'
2672
values.update(state='assigned')
2673
# copy stock.move with new product_qty, qty_per_pack. from_pack, to_pack, pack_type, length, width, height, weight
2674
new_move = move_obj.copy(cr, uid, move, values, context=context)
2675
# Need to change the locations after the copy, because the create of a new stock move with
2676
# non-stockable product force the locations
2677
move_obj.write(cr, uid, [new_move], {'location_id': moves[move].location_id.id,
2678
'location_dest_id': moves[move].location_dest_id.id}, context=context)
2680
# update the existing stock move
2681
updated[move] = {'initial': moves[move].product_qty, 'partial_qty': partial['product_qty']}
2682
move_obj.write(cr, uid, [move], values, context=context)
2684
# integrity check - all moves are treated and no more
2685
assert set(from_pick) == set(from_partial), 'move_ids are not equal pick:%s - partial:%s'%(set(from_pick), set(from_partial))
2686
# quantities are right
2687
assert all([updated[m]['initial'] == updated[m]['partial_qty'] for m in updated.keys()]), 'initial quantity is not equal to the sum of partial quantities (%s).'%(updated)
2688
# copy to 'packing' stock.picking
2689
# draft shipment is automatically created or updated if a shipment already
2690
pack_number = pick.name.split("/")[1]
2691
new_packing_id = self.copy(cr, uid, pick.id, {'name': 'PACK/' + pack_number,
2692
'subtype': 'packing',
2693
'previous_step_id': pick.id,
2694
'backorder_id': False,
2695
'shipment_id': False}, context=dict(context, keep_prodlot=True, allow_copy=True,))
2697
self.write(cr, uid, [new_packing_id], {'origin': pick.origin}, context=context)
2698
# update locations of stock moves and state as the picking stay at 'draft' state.
2699
# if return move have been done in previous ppl step, we remove the corresponding copied move (criteria: qty_per_pack == 0)
2700
new_packing = self.browse(cr, uid, new_packing_id, context=context)
2701
for move in new_packing.move_lines:
2702
if move.qty_per_pack == 0:
2703
move_obj.unlink(cr, uid, [move.id], context=context)
2705
move.write({'state': 'assigned',
2706
'location_id': new_packing.warehouse_id.lot_dispatch_id.id,
2707
'location_dest_id': new_packing.warehouse_id.lot_distribution_id.id}, context=context)
2709
# trigger standard workflow
2710
self.action_move(cr, uid, [pick.id])
2711
wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
2713
# TODO which behavior
2714
#return {'type': 'ir.actions.act_window_close'}
2715
data_obj = self.pool.get('ir.model.data')
2716
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_shipment_form')
2717
view_id = view_id and view_id[1] or False
2718
return {'name':_("Shipment"),
2719
'view_mode': 'form,tree',
2720
'view_id': [view_id],
2721
'view_type': 'form',
2722
'res_model': 'shipment',
2723
'res_id': new_packing.shipment_id.id,
2724
'type': 'ir.actions.act_window',
2729
def return_products(self, cr, uid, ids, context=None):
2731
open the return products wizard
2733
# we need the context
2738
name = _("Return Products")
2739
model = 'create.picking'
2740
step = 'returnproducts'
2741
wiz_obj = self.pool.get('wizard')
2742
# open the selected wizard
2743
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=context)
2745
def do_return_products(self, cr, uid, ids, context=None):
2748
- update the draft picking ticket
2749
- create the back move
2754
assert context, 'context not defined'
2755
assert 'partial_datas' in context, 'partial_datas no defined in context'
2756
partial_datas = context['partial_datas']
2758
move_obj = self.pool.get('stock.move')
2759
wf_service = netsvc.LocalService("workflow")
2761
draft_picking_id = False
2762
for picking in self.browse(cr, uid, ids, context=context):
2764
# corresponding draft picking ticket
2765
draft_picking_id = picking.previous_step_id.backorder_id.id
2767
for move in move_obj.browse(cr, uid, partial_datas[picking.id].keys(), context=context):
2768
# we browse the updated moves (return qty > 0 is checked during data generation)
2770
data = partial_datas[picking.id][move.id]
2773
return_qty = data['qty_to_return']
2774
# initial qty is decremented
2775
initial_qty = move.product_qty
2776
initial_qty = max(initial_qty - return_qty, 0)
2777
values = {'product_qty': initial_qty}
2780
# if all products are sent back to stock, the move state is cancel - done for now, ideologic question, wahouuu!
2781
#values.update({'state': 'cancel'})
2782
values.update({'state': 'done'})
2783
move_obj.write(cr, uid, [move.id], values, context=context)
2785
# create a back move with the quantity to return to the good location
2786
# the good location is stored in the 'initial_location' field
2787
move_obj.copy(cr, uid, move.id, {'product_qty': return_qty,
2788
'location_dest_id': move.initial_location.id,
2789
'state': 'done'}, context=dict(context, keepLineNumber=True))
2791
# increase the draft move with the move quantity
2792
draft_move_id = move.backmove_id.id
2793
draft_initial_qty = move_obj.read(cr, uid, [draft_move_id], ['product_qty'], context=context)[0]['product_qty']
2794
draft_initial_qty += return_qty
2795
move_obj.write(cr, uid, [draft_move_id], {'product_qty': draft_initial_qty}, context=context)
2797
# log the increase action - display the picking ticket view form
2798
# TODO refactoring needed
2799
obj_data = self.pool.get('ir.model.data')
2800
res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_ppl_form')[1]
2801
self.log(cr, uid, picking.id, _("Products from Pre-Packing List (%s) have been returned to stock.")%(picking.name,), context={'view_id': res,})
2802
res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
2803
self.log(cr, uid, draft_picking_id, _("The corresponding Draft Picking Ticket (%s) has been updated.")%(picking.previous_step_id.backorder_id.name,), context={'view_id': res,})
2804
# if all moves are done or canceled, the ppl is canceled
2806
for move in picking.move_lines:
2807
if move.state in ('assigned'):
2811
# we dont want the back move (done) to be canceled - so we dont use the original cancel workflow state because
2812
# action_cancel() from stock_picking would be called, this would cancel the done stock_moves
2813
# instead we move to the new return_cancel workflow state which simply set the stock_picking state to 'cancel'
2814
# TODO THIS DOESNT WORK - still done state - replace with trigger for now
2815
#wf_service.trg_validate(uid, 'stock.picking', picking.id, 'return_cancel', cr)
2816
wf_service.trg_write(uid, 'stock.picking', picking.id, cr)
2818
# TODO which behavior
2819
#return {'type': 'ir.actions.act_window_close'}
2820
data_obj = self.pool.get('ir.model.data')
2821
view_id = data_obj.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')
2822
view_id = view_id and view_id[1] or False
2823
context.update({'picking_type': 'picking_ticket'})
2825
'name':_("Picking Ticket"),
2826
'view_mode': 'form,tree',
2827
'view_id': [view_id],
2828
'view_type': 'form',
2829
'res_model': 'stock.picking',
2830
'res_id': draft_picking_id ,
2831
'type': 'ir.actions.act_window',
2836
def action_cancel(self, cr, uid, ids, context=None):
2838
override cancel state action from the workflow
2840
- depending on the subtype and state of the stock.picking object
2841
the behavior will be different
2843
Cancel button is active for the picking object:
2844
- subtype: 'picking'
2845
Cancel button is active for the shipment object:
2846
- subtype: 'packing'
2848
state is not taken into account as picking is canceled before
2852
move_obj = self.pool.get('stock.move')
2853
obj_data = self.pool.get('ir.model.data')
2855
# check the state of the picking
2856
for picking in self.browse(cr, uid, ids, context=context):
2857
# if draft and shipment is in progress, we cannot cancel
2858
if picking.subtype == 'picking' and picking.state in ('draft',):
2859
if self.has_picking_ticket_in_progress(cr, uid, [picking.id], context=context)[picking.id]:
2860
raise osv.except_osv(_('Warning !'), _('Some Picking Tickets are in progress. Return products to stock from ppl and shipment and try to cancel again.'))
2861
return super(stock_picking, self).action_cancel(cr, uid, ids, context=context)
2862
# if not draft or qty does not match, the shipping is already in progress
2863
if picking.subtype == 'picking' and picking.state in ('done',):
2864
raise osv.except_osv(_('Warning !'), _('The shipment process is completed and cannot be canceled!'))
2866
# first call to super method, so if some checks fail won't perform other actions anyway
2867
# call super - picking is canceled
2868
super(stock_picking, self).action_cancel(cr, uid, ids, context=context)
2870
for picking in self.browse(cr, uid, ids, context=context):
2872
if picking.subtype == 'picking':
2874
# get the draft picking
2875
draft_picking_id = picking.backorder_id.id
2877
# for each move from picking ticket - could be split moves
2878
for move in picking.move_lines:
2879
# find the corresponding move in draft
2880
draft_move = move.backmove_id
2881
# increase the draft move with the move quantity
2882
initial_qty = move_obj.read(cr, uid, [draft_move.id], ['product_qty'], context=context)[0]['product_qty']
2883
initial_qty += move.product_qty
2884
move_obj.write(cr, uid, [draft_move.id], {'product_qty': initial_qty}, context=context)
2885
# log the increase action
2886
# TODO refactoring needed
2887
obj_data = self.pool.get('ir.model.data')
2888
res = obj_data.get_object_reference(cr, uid, 'msf_outgoing', 'view_picking_ticket_form')[1]
2889
self.log(cr, uid, draft_picking_id, _("The corresponding Draft Picking Ticket (%s) has been updated.")%(picking.backorder_id.name,), context={'view_id': res,})
2891
if picking.subtype == 'packing':
2892
# for each packing we get the draft packing
2893
draft_packing_id = picking.backorder_id.id
2895
# for each move from the packing
2896
for move in picking.move_lines:
2897
# corresponding draft move from draft **packing** object
2898
draft_move_id = move.backmove_packing_id.id
2899
# check the to_pack of draft move
2900
# if equal to draft to_pack = move from_pack - 1 (as we always take the pack with the highest number available)
2901
# we can increase the qty and update draft to_pack
2902
# otherwise we copy the draft packing move with updated quantity and from/to
2903
# we always create a new move
2904
draft_read = move_obj.read(cr, uid, [draft_move_id], ['product_qty', 'to_pack'], context=context)[0]
2905
draft_to_pack = draft_read['to_pack']
2906
if draft_to_pack + 1 == move.from_pack and False: # DEACTIVATED
2908
draft_qty = draft_read['product_qty'] + move.product_qty
2909
# update the draft move
2910
move_obj.write(cr, uid, [draft_move_id], {'product_qty': draft_qty, 'to_pack': move.to_pack}, context=context)
2912
# copy draft move (to be sure not to miss original info) with move qty and from/to
2913
move_obj.copy(cr, uid, draft_move_id, {'product_qty': move.product_qty,
2914
'from_pack': move.from_pack,
2915
'to_pack': move.to_pack,
2916
'state': 'assigned'}, context=context)
2923
class wizard(osv.osv):
2925
class offering open_wizard method for wizard control
2929
def open_wizard(self, cr, uid, ids, name=False, model=False, step='default', type='create', context=None):
2931
WARNING : IDS CORRESPOND TO ***MAIN OBJECT IDS*** (picking for example) take care when calling the method from wizards
2932
return the newly created wizard's id
2933
name, model, step are mandatory only for type 'create'
2938
if type == 'create':
2939
assert name, 'type "create" and no name defined'
2940
assert model, 'type "create" and no model defined'
2941
assert step, 'type "create" and no step defined'
2942
# create the memory object - passing the picking id to it through context
2943
wizard_id = self.pool.get(model).create(
2944
cr, uid, {}, context=dict(context,
2948
back_model=context.get('model', False),
2949
back_wizard_ids=context.get('wizard_ids', False),
2950
back_wizard_name=context.get('wizard_name', False),
2951
back_step=context.get('step', False),
2954
elif type == 'back':
2955
# open the previous wizard
2956
assert context['back_wizard_ids'], 'no back_wizard_ids defined'
2957
wizard_id = context['back_wizard_ids'][0]
2958
assert context['back_wizard_name'], 'no back_wizard_name defined'
2959
name = context['back_wizard_name']
2960
assert context['back_model'], 'no back_model defined'
2961
model = context['back_model']
2962
assert context['back_step'], 'no back_step defined'
2963
step = context['back_step']
2965
elif type == 'update':
2966
# refresh the same wizard
2967
assert context['wizard_ids'], 'no wizard_ids defined'
2968
wizard_id = context['wizard_ids'][0]
2969
assert context['wizard_name'], 'no wizard_name defined'
2970
name = context['wizard_name']
2971
assert context['model'], 'no model defined'
2972
model = context['model']
2973
assert context['step'], 'no step defined'
2974
step = context['step']
2976
# call action to wizard view
2979
'view_mode': 'form',
2981
'view_type': 'form',
2983
'res_id': wizard_id,
2984
'type': 'ir.actions.act_window',
2988
'context': dict(context,
2990
wizard_ids=[wizard_id],
2993
back_model=context.get('model', False),
2994
back_wizard_ids=context.get('wizard_ids', False),
2995
back_wizard_name=context.get('wizard_name', False),
2996
back_step=context.get('step', False),
3003
class product_product(osv.osv):
3005
add a getter for keep cool notion
3007
_inherit = 'product.product'
3009
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
3011
get functional values
3014
for product in self.browse(cr, uid, ids, context=context):
3015
values = {'is_keep_cool': False,
3017
result[product.id] = values
3019
is_keep_cool = bool(product.heat_sensitive_item)# in ('*', '**', '***',)
3020
values['is_keep_cool'] = is_keep_cool
3024
_columns = {'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals',),
3025
'prodlot_ids': fields.one2many('stock.production.lot', 'product_id', string='Batch Numbers',),
3031
class stock_move(osv.osv):
3035
_inherit = 'stock.move'
3037
def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
3039
facade for product_available function from product (stock)
3041
# get the corresponding product ids
3043
for d in self.read(cr, uid, ids, ['product_id'], context):
3044
result[d['id']] = d['product_id'][0]
3046
# get the virtual stock identified by product ids
3047
virtual = self.pool.get('product.product')._product_available(cr, uid, result.values(), field_names, arg, context)
3049
# replace product ids by corresponding stock move id
3050
result = dict([id, virtual[result[id]]] for id in result.keys())
3053
def _vals_get(self, cr, uid, ids, fields, arg, context=None):
3055
get functional values
3058
for move in self.browse(cr, uid, ids, context=context):
3059
values = {'qty_per_pack': 0.0,
3060
'total_amount': 0.0,
3062
'currency_id': False,
3064
'is_dangerous_good': False,
3065
'is_keep_cool': False,
3066
'is_narcotic': False,
3067
'sale_order_line_number': 0,
3069
result[move.id] = values
3070
# number of packs with from/to values (integer)
3071
if move.to_pack == 0:
3074
num_of_packs = move.to_pack - move.from_pack + 1
3075
values['num_of_packs'] = num_of_packs
3078
values['qty_per_pack'] = move.product_qty / num_of_packs
3080
values['qty_per_pack'] = 0
3081
# total amount (float)
3082
total_amount = move.sale_line_id and move.sale_line_id.price_unit * move.product_qty or 0.0
3083
values['total_amount'] = total_amount
3084
# amount for one pack
3086
amount = total_amount / num_of_packs
3089
values['amount'] = amount
3091
values['currency_id'] = move.sale_line_id and move.sale_line_id.currency_id and move.sale_line_id.currency_id.id or False
3093
values['is_dangerous_good'] = move.product_id and move.product_id.dangerous_goods or False
3094
# keep cool - if heat_sensitive_item is True
3095
values['is_keep_cool'] = bool(move.product_id and move.product_id.heat_sensitive_item or False)
3097
values['is_narcotic'] = move.product_id and move.product_id.narcotic or False
3098
# sale_order_line_number
3099
values['sale_order_line_number'] = move.sale_line_id and move.sale_line_id.line_number or 0
3103
def default_get(self, cr, uid, fields, context=None):
3105
Set default values according to type and subtype
3110
res = super(stock_move, self).default_get(cr, uid, fields, context=context)
3112
if 'warehouse_id' in context and context.get('warehouse_id'):
3113
warehouse_id = context.get('warehouse_id')
3115
warehouse_id = self.pool.get('stock.warehouse').search(cr, uid, [], context=context)[0]
3116
res.update({'location_output_id': self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_output_id.id})
3118
loc_virtual_ids = self.pool.get('stock.location').search(cr, uid, [('name', '=', 'Virtual Locations')])
3119
loc_virtual_id = len(loc_virtual_ids) > 0 and loc_virtual_ids[0] or False
3120
res.update({'location_virtual_id': loc_virtual_id})
3122
if 'type' in context and context.get('type', False) == 'out':
3123
loc_stock_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_stock_id.id
3124
res.update({'location_id': loc_stock_id})
3126
if 'subtype' in context and context.get('subtype', False) == 'picking':
3127
loc_packing_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_packing_id.id
3128
res.update({'location_dest_id': loc_packing_id})
3129
elif 'subtype' in context and context.get('subtype', False) == 'standard':
3130
loc_output_id = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context).lot_output_id.id
3131
res.update({'location_dest_id': loc_output_id})
3135
_columns = {'from_pack': fields.integer(string='From p.'),
3136
'to_pack': fields.integer(string='To p.'),
3137
'pack_type': fields.many2one('pack.type', string='Pack Type'),
3138
'length' : fields.float(digits=(16,2), string='Length [cm]'),
3139
'width' : fields.float(digits=(16,2), string='Width [cm]'),
3140
'height' : fields.float(digits=(16,2), string='Height [cm]'),
3141
'weight' : fields.float(digits=(16,2), string='Weight p.p [kg]'),
3142
#'pack_family_id': fields.many2one('pack.family', string='Pack Family'),
3143
'initial_location': fields.many2one('stock.location', string='Initial Picking Location'),
3144
# relation to the corresponding move from draft **picking** ticket object
3145
'backmove_id': fields.many2one('stock.move', string='Corresponding move of previous step'),
3146
# relation to the corresponding move from draft **packing** ticket object
3147
'backmove_packing_id': fields.many2one('stock.move', string='Corresponding move of previous step in draft packing'),
3149
'virtual_available': fields.function(_product_available, method=True, type='float', string='Virtual Stock', help="Future stock for this product according to the selected locations or all internal if none have been selected. Computed as: Real Stock - Outgoing + Incoming.", multi='qty_available', digits_compute=dp.get_precision('Product UoM')),
3150
'qty_per_pack': fields.function(_vals_get, method=True, type='float', string='Qty p.p', multi='get_vals',),
3151
'total_amount': fields.function(_vals_get, method=True, type='float', string='Total Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals',),
3152
'amount': fields.function(_vals_get, method=True, type='float', string='Pack Amount', digits_compute=dp.get_precision('Picking Price'), multi='get_vals',),
3153
'num_of_packs': fields.function(_vals_get, method=True, type='integer', string='#Packs', multi='get_vals_X',), # old_multi get_vals
3154
'currency_id': fields.function(_vals_get, method=True, type='many2one', relation='res.currency', string='Currency', multi='get_vals',),
3155
'is_dangerous_good': fields.function(_vals_get, method=True, type='boolean', string='Dangerous Good', multi='get_vals',),
3156
'is_keep_cool': fields.function(_vals_get, method=True, type='boolean', string='Keep Cool', multi='get_vals',),
3157
'is_narcotic': fields.function(_vals_get, method=True, type='boolean', string='Narcotic', multi='get_vals',),
3158
'sale_order_line_number': fields.function(_vals_get, method=True, type='integer', string='Sale Order Line Number', multi='get_vals_X',), # old_multi get_vals
3159
# Fields used for domain
3160
'location_virtual_id': fields.many2one('stock.location', string='Virtual location'),
3161
'location_output_id': fields.many2one('stock.location', string='Output location'),
3162
'invoice_line_id': fields.many2one('account.invoice.line', string='Invoice line'),
3165
def action_cancel(self, cr, uid, ids, context=None):
3167
Confirm or check the procurement order associated to the stock move
3169
res = super(stock_move, self).action_cancel(cr, uid, ids, context=context)
3171
wf_service = netsvc.LocalService("workflow")
3173
proc_obj = self.pool.get('procurement.order')
3174
proc_ids = proc_obj.search(cr, uid, [('move_id', 'in', ids)], context=context)
3175
for proc in proc_obj.browse(cr, uid, proc_ids, context=context):
3176
if proc.state == 'draft':
3177
wf_service.trg_validate(uid, 'procurement.order', proc.id, 'button_confirm', cr)
3179
wf_service.trg_validate(uid, 'procurement.order', proc.id, 'button_check', cr)
3186
class sale_order(osv.osv):
3188
re-override to modify behavior for outgoing workflow
3190
_inherit = 'sale.order'
3191
_name = 'sale.order'
3193
def _hook_ship_create_execute_specific_code_01(self, cr, uid, ids, context=None, *args, **kwargs):
3195
Please copy this to your module's method also.
3196
This hook belongs to the action_ship_create method from sale>sale.py
3198
- allow to execute specific code at position 01
3200
super(sale_order, self)._hook_ship_create_execute_specific_code_01(cr, uid, ids, context=context, *args, **kwargs)
3202
pick_obj = self.pool.get('stock.picking')
3204
wf_service = netsvc.LocalService("workflow")
3205
proc_id = kwargs['proc_id']
3206
order = kwargs['order']
3207
if order.procurement_request :
3208
proc = self.pool.get('procurement.order').browse(cr, uid, [proc_id], context=context)
3209
pick_id = proc and proc[0] and proc[0].move_id and proc[0].move_id.picking_id and proc[0].move_id.picking_id.id or False
3211
wf_service.trg_validate(uid, 'stock.picking', [pick_id], 'button_confirm', cr)
3213
# We also do a first 'check availability': cancel then check
3214
pick_obj.cancel_assign(cr, uid, [pick_id], context)
3215
pick_obj.action_assign(cr, uid, [pick_id], context)
3219
def _hook_ship_create_execute_specific_code_02(self, cr, uid, ids, context=None, *args, **kwargs):
3221
Please copy this to your module's method also.
3222
This hook belongs to the action_ship_create method from sale>sale.py
3224
- allow to execute specific code at position 02
3226
# Some verifications
3229
if isinstance(ids, (int, long)):
3233
pick_obj = self.pool.get('stock.picking')
3234
move_obj = self.pool.get('stock.move')
3235
order = kwargs['order']
3236
move_id = kwargs['move_id']
3237
pick_id = move_obj.browse(cr, uid, [move_id], context=context)[0].picking_id.id
3239
if order.procurement_request:
3240
move_obj.action_confirm(cr, uid, [move_id], context=context)
3241
# we Validate the picking "Confirms picking directly from draft state."
3242
pick_obj.draft_force_assign(cr, uid , [pick_id], context)
3243
# We also do a first 'check availability': cancel then check
3244
pick_obj.cancel_assign(cr, uid, [pick_id], context)
3245
pick_obj.action_assign(cr, uid, [pick_id], context)
3247
return super(sale_order, self)._hook_ship_create_execute_specific_code_02(cr, uid, ids, context, *args, **kwargs)
3249
def _hook_ship_create_stock_move(self, cr, uid, ids, context=None, *args, **kwargs):
3251
Please copy this to your module's method also.
3252
This hook belongs to the action_ship_create method from sale>sale.py
3254
- allow to modify the data for stock move creation
3256
move_data = super(sale_order, self)._hook_ship_create_stock_move(cr, uid, ids, context=context, *args, **kwargs)
3257
order = kwargs['order']
3259
if self.read(cr, uid, ids, ['procurement_request'], context=context)[0]['procurement_request']\
3260
and self.read(cr, uid, ids, ['location_requestor_id'], context=context)[0]['location_requestor_id']:
3261
move_data['type'] = 'internal'
3262
move_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
3263
move_data['location_dest_id'] = self.read(cr, uid, ids, ['location_requestor_id'], context=context)[0]['location_requestor_id'][0]
3265
# first go to packing location (PICK/PACK/SHIP) or output location (Simple OUT)
3266
# according to the configuration
3267
# first go to packing location
3268
setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
3269
if setup.delivery_process == 'simple':
3270
move_data['location_dest_id'] = order.shop_id.warehouse_id.lot_output_id.id
3272
move_data['location_dest_id'] = order.shop_id.warehouse_id.lot_packing_id.id
3274
if self.pool.get('product.product').browse(cr, uid, move_data['product_id']).type == 'service_recep':
3275
move_data['location_id'] = self.pool.get('stock.location').get_cross_docking_location(cr, uid)
3277
if 'sale_line_id' in move_data and move_data['sale_line_id']:
3278
sale_line = self.pool.get('sale.order.line').browse(cr, uid, move_data['sale_line_id'], context=context)
3279
if sale_line.type == 'make_to_order':
3280
move_data['location_id'] = self.pool.get('stock.location').get_cross_docking_location(cr, uid)
3281
move_data['move_cross_docking_ok'] = True
3282
# Update the stock.picking
3283
self.pool.get('stock.picking').write(cr, uid, move_data['picking_id'], {'cross_docking_ok': True}, context=context)
3285
move_data['state'] = 'confirmed'
3288
def _hook_ship_create_stock_picking(self, cr, uid, ids, context=None, *args, **kwargs):
3290
Please copy this to your module's method also.
3291
This hook belongs to the action_ship_create method from sale>sale.py
3293
- allow to modify the data for stock picking creation
3295
setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
3297
picking_data = super(sale_order, self)._hook_ship_create_stock_picking(cr, uid, ids, context=context, *args, **kwargs)
3298
order = kwargs['order']
3300
picking_data['state'] = 'draft'
3301
if setup.delivery_process == 'simple':
3302
picking_data['subtype'] = 'standard'
3303
# use the name according to picking ticket sequence
3304
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
3306
picking_data['subtype'] = 'picking'
3307
# use the name according to picking ticket sequence
3308
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'picking.ticket')
3311
if self.read(cr, uid, ids, ['procurement_request'], context=context):
3312
procurement_request = self.read(cr, uid, ids, ['procurement_request'], context=context)[0]['procurement_request']
3313
if procurement_request:
3314
picking_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
3315
picking_data['type'] = 'internal'
3316
picking_data['subtype'] = 'standard'
3317
picking_data['reason_type_id'] = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'reason_types_moves', 'reason_type_internal_supply')[1]
3318
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.internal')
3320
picking_data['name'] = pick_name
3321
picking_data['flow_type'] = 'full'
3322
picking_data['backorder_id'] = False
3323
picking_data['warehouse_id'] = order.shop_id.warehouse_id.id
3327
def _hook_ship_create_execute_picking_workflow(self, cr, uid, ids, context=None, *args, **kwargs):
3329
Please copy this to your module's method also.
3330
This hook belongs to the action_ship_create method from sale>sale.py
3332
- allow to avoid the stock picking workflow execution
3333
- trigger the logging message for the created picking, as it stays in draft state and no call to action_confirm is performed
3334
for the moment within the msf_outgoing logic
3336
setup = self.pool.get('unifield.setup.configuration').get_config(cr, uid)
3337
cond = super(sale_order, self)._hook_ship_create_execute_picking_workflow(cr, uid, ids, context=context, *args, **kwargs)
3339
# On Simple OUT configuration, the system should confirm the OUT and launch a first check availability
3340
if setup.delivery_process != 'simple':
3341
cond = cond and False
3343
# diplay creation message for draft picking ticket
3344
picking_id = kwargs['picking_id']
3345
picking_obj = self.pool.get('stock.picking')
3347
picking_obj.log_picking(cr, uid, [picking_id], context=context)
3348
# Launch a first check availability
3349
self.pool.get('stock.picking').action_assign(cr, uid, [picking_id], context=context)
3356
class procurement_order(osv.osv):
3358
procurement order workflow
3360
_inherit = 'procurement.order'
3362
def _hook_check_mts_on_message(self, cr, uid, context=None, *args, **kwargs):
3364
Please copy this to your module's method also.
3365
This hook belongs to the _check_make_to_stock_product method from procurement>procurement.py>procurement.order
3367
- allow to modify the message written back to procurement order
3369
message = super(procurement_order, self)._hook_check_mts_on_message(cr, uid, context=context, *args, **kwargs)
3370
procurement = kwargs['procurement']
3371
if procurement.move_id.picking_id.state == 'draft' and procurement.move_id.picking_id.subtype == 'picking':
3372
message = _("Shipment Process in Progress.")