1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 TeMPO Consulting, MSF
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
26
from tools.translate import _
27
from dateutil.relativedelta import relativedelta
28
from datetime import datetime
33
class stock_move(osv.osv):
35
new function to get mirror move
37
_inherit = 'stock.move'
38
_columns = {'line_number': fields.integer(string='Line', required=True),
39
'change_reason': fields.char(string='Change Reason', size=1024, readonly=True),
41
_defaults = {'line_number': 0,}
42
_order = 'line_number'
44
def create(self, cr, uid, vals, context=None):
46
add the corresponding line number
48
if a corresponding purchase order line or sale order line exist
49
we take the line number from there
52
picking_obj = self.pool.get('stock.picking')
53
pol_obj = self.pool.get('purchase.order.line')
54
sol_obj = self.pool.get('sale.order.line')
55
seq_pool = self.pool.get('ir.sequence')
57
# line number correspondance to be checked with Magali
58
if vals.get('picking_id'):
59
if vals.get('purchase_line_id') and False:
60
# from purchase order line
61
line = pol_obj.read(cr, uid, [vals.get('purchase_line_id')], ['line_number'], context=context)[0]['line_number']
62
elif vals.get('sale_line_id') and False:
63
# from sale order line
64
line = sol_obj.read(cr, uid, [vals.get('sale_line_id')], ['line_number'], context=context)[0]['line_number']
66
# new numbers - gather the line number from the sequence
67
sequence_id = picking_obj.read(cr, uid, [vals['picking_id']], ['move_sequence_id'], context=context)[0]['move_sequence_id'][0]
68
line = seq_pool.get_id(cr, uid, sequence_id, test='id', context=context)
69
# update values with line value
70
vals.update({'line_number': line})
72
# create the new object
73
result = super(stock_move, self).create(cr, uid, vals, context=context)
76
def get_mirror_move(self, cr, uid, ids, data_back, context=None):
78
return a dictionary with IN for OUT and OUT for IN, if exists, False otherwise
80
only one mirror object should exist for each object (to check)
81
return objects which are not done
83
same sale_line_id/purchase_line_id - same product - same quantity
85
IN: move -> po line -> procurement -> so line -> move
86
OUT: move -> so line -> procurement -> po line -> move
88
I dont use move.move_dest_id because of back orders both on OUT and IN sides
92
if isinstance(ids, (int, long)):
96
so_line_obj = self.pool.get('sale.order.line')
99
for obj in self.browse(cr, uid, ids, context=context):
101
if obj.picking_id and obj.picking_id.type == 'in':
102
# we are looking for corresponding OUT move from sale order line
103
if obj.purchase_line_id:
105
if obj.purchase_line_id.procurement_id:
107
procurement_id = obj.purchase_line_id.procurement_id.id
108
# find the corresponding sale order line
109
so_line_ids = so_line_obj.search(cr, uid, [('procurement_id', '=', procurement_id)], context=context)
110
# if the procurement comes from replenishment rules, there will be a procurement, but no associated sale order line
111
# we therefore do not raise an exception, but handle the case only if sale order lines are found
113
# find the corresponding OUT move
114
# move_ids = self.search(cr, uid, [('product_id', '=', obj.product_id.id), ('product_qty', '=', obj.product_qty), ('state', 'in', ('assigned', 'confirmed')), ('sale_line_id', '=', so_line_ids[0])], context=context)
115
move_ids = self.search(cr, uid, [('product_id', '=', data_back['product_id']), ('state', 'in', ('assigned', 'confirmed')), ('sale_line_id', '=', so_line_ids[0])], context=context)
116
# list of matching out moves
118
for move in self.browse(cr, uid, move_ids, context=context):
119
# move from draft picking or standard picking
120
if (move.picking_id.subtype == 'picking' and not move.picking_id.backorder_id and move.picking_id.state == 'draft') or (move.picking_id.subtype == 'standard') and move.picking_id.type == 'out':
121
integrity_check.append(move.id)
122
# return the first one matching
124
res[obj.id] = integrity_check[0]
126
# we are looking for corresponding IN from on_order purchase order
127
assert False, 'This method is not implemented for OUT or Internal moves'
134
class stock_picking(osv.osv):
136
do_partial modification
138
_inherit = 'stock.picking'
139
_columns = {'move_sequence_id': fields.many2one('ir.sequence', string='Moves Sequence', help="This field contains the information related to the numbering of the moves of this picking.", required=True, ondelete='cascade'),
140
'change_reason': fields.char(string='Change Reason', size=1024, readonly=True),
143
def _stock_picking_action_process_hook(self, cr, uid, ids, context=None, *args, **kwargs):
145
Please copy this to your module's method also.
146
This hook belongs to the action_process method from stock>stock.py>stock_picking
148
- allow to modify the data for wizard display
152
if isinstance(ids, (int, long)):
154
res = super(stock_picking, self)._stock_picking_action_process_hook(cr, uid, ids, context=context, *args, **kwargs)
155
wizard_obj = self.pool.get('wizard')
156
res = wizard_obj.open_wizard(cr, uid, ids, type='update', context=dict(context,
157
wizard_ids=[res['res_id']],
158
wizard_name=res['name'],
159
model=res['res_model'],
163
def create(self, cr, uid, vals, context=None):
165
create the sequence for the numbering of the lines
168
seq_pool = self.pool.get('ir.sequence')
169
po_obj = self.pool.get('purchase.order')
170
so_obj = self.pool.get('sale.order')
172
new_seq_id = self.create_sequence(cr, uid, vals, context=context)
173
vals.update({'move_sequence_id': new_seq_id,})
174
# if from order, we udpate the sequence to match the order's one
175
# line number correspondance to be checked with Magali
177
if vals.get('purchase_id') and False:
178
seq_id = po_obj.read(cr, uid, [vals.get('purchase_id')], ['sequence_id'], context=context)[0]['sequence_id'][0]
179
seq_value = seq_pool.read(cr, uid, [seq_id], ['number_next'], context=context)[0]['number_next']
180
elif vals.get('sale_id') and False:
181
seq_id = po_obj.read(cr, uid, [vals.get('sale_id')], ['sequence_id'], context=context)[0]['sequence_id'][0]
182
seq_value = seq_pool.read(cr, uid, [seq_id], ['number_next'], context=context)[0]['number_next']
185
# update sequence value of stock picking to match order's one
186
seq_pool.write(cr, uid, [new_seq_id], {'number_next': seq_value,})
188
return super(stock_picking, self).create(cr, uid, vals, context=context)
190
def create_data_back(self, cr, uid, move, context=None):
192
build data_back dictionary
194
res = {'id': move.id,
195
'name': move.product_id.partner_ref,
196
'product_id': move.product_id.id,
197
'product_uom': move.product_uom.id,
198
'product_qty': move.product_qty,
202
def _update_mirror_move(self, cr, uid, ids, data_back, diff_qty, out_move=False, context=None):
204
update the mirror move with difference quantity diff_qty
206
if out_move is provided, it is used for copy if another cannot be found (meaning the one provided does
209
# NOTE: the price is not update in OUT move according to average price computation. this is an open point.
211
if diff_qty < 0, the qty is decreased
212
if diff_qty > 0, the qty is increased
215
move_obj = self.pool.get('stock.move')
216
product_obj = self.pool.get('product.product')
217
# first look for a move - we search even if we get out_move because out_move
218
# may not be valid anymore (product changed) - get_mirror_move will validate it or return nothing
219
out_move_id = move_obj.get_mirror_move(cr, uid, [data_back['id']], data_back, context=context)[data_back['id']]
220
if not out_move_id and out_move:
221
# copy existing out_move with move properties: - update the name of the stock move
222
# the state is confirmed, we dont know if available yet - should be in input location before stock
223
values = {'name': data_back['name'],
224
'product_id': data_back['product_id'],
226
'product_uos_qty': 0,
227
'product_uom': data_back['product_uom'],
228
'state': 'confirmed',
230
out_move_id = move_obj.copy(cr, uid, out_move, values, context=context)
233
# decrease/increase depending on diff_qty sign the qty by diff_qty
234
data = move_obj.read(cr, uid, [out_move_id], ['product_qty', 'picking_id', 'name', 'product_uom'], context=context)[0]
235
picking_out_name = data['picking_id'][1]
236
stock_move_name = data['name']
237
uom_name = data['product_uom'][1]
238
present_qty = data['product_qty']
239
new_qty = max(present_qty + diff_qty, 0)
240
move_obj.write(cr, uid, [out_move_id], {'product_qty' : new_qty,
241
'product_uos_qty': new_qty,}, context=context)
242
# log the modification
243
# log creation message
244
move_obj.log(cr, uid, out_move_id, _('The Stock Move %s from %s has been updated to %s %s.')%(stock_move_name, picking_out_name, new_qty, uom_name))
245
# return updated move or False
248
def _do_incoming_shipment_first_hook(self, cr, uid, ids, context=None, *args, **kwargs):
250
hook to update values for stock move if first encountered
252
values = kwargs.get('values')
253
assert values is not None, 'missing values'
257
def do_incoming_shipment(self, cr, uid, ids, context=None):
259
validate the picking ticket from selected stock moves
261
move here the logic of validate picking
262
available for picking loop
264
assert context, 'context is not defined'
265
assert 'partial_datas' in context, 'partial datas not present in context'
266
partial_datas = context['partial_datas']
267
if isinstance(ids, (int, long)):
271
sequence_obj = self.pool.get('ir.sequence')
273
move_obj = self.pool.get('stock.move')
274
product_obj = self.pool.get('product.product')
275
currency_obj = self.pool.get('res.currency')
276
uom_obj = self.pool.get('product.uom')
277
# create picking object
278
create_picking_obj = self.pool.get('create.picking')
280
wf_service = netsvc.LocalService("workflow")
282
internal_loc_ids = self.pool.get('stock.location').search(cr, uid, [('usage','=','internal')])
283
ctx_avg = context.copy()
284
ctx_avg['location'] = internal_loc_ids
285
for pick in self.browse(cr, uid, ids, context=context):
286
# corresponding backorder object - not necessarily created
289
move_ids = partial_datas[pick.id].keys()
291
all_move_ids = [move.id for move in pick.move_lines]
292
# related moves - swap if a backorder is created - openERP logic
294
# average price computation
296
for move in move_obj.browse(cr, uid, move_ids, context=context):
297
# keep data for back order creation
298
data_back = self.create_data_back(cr, uid, move, context=context)
301
# flag to update the first move - if split was performed during the validation, new stock moves are created
303
# force complete flag = validate all partial for the same move have the same force complete value
304
force_complete = False
306
initial_qty = move.product_qty
307
# corresponding out move
308
out_move_id = move_obj.get_mirror_move(cr, uid, [move.id], data_back, context=context)[move.id]
310
update_out = (len(partial_datas[pick.id][move.id]) > 1)
311
# average price computation, new values - should be the same for every partial
314
for partial in partial_datas[pick.id][move.id]:
315
# original openERP logic - average price computation - To be validated by Matthias
316
# Average price computation
317
# selected product from wizard must be tested
318
product = product_obj.browse(cr, uid, partial['product_id'], context=ctx_avg)
319
if (pick.type == 'in') and (product.cost_method == 'average'):
320
move_currency_id = move.company_id.currency_id.id
321
context['currency_id'] = move_currency_id
323
product_uom = partial['product_uom']
324
product_qty = partial['product_qty']
325
product_currency = partial['product_currency']
326
product_price = partial['product_price']
327
qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
329
if product.id in product_avail:
330
product_avail[product.id] += qty
332
product_avail[product.id] = product.qty_available
335
new_price = currency_obj.compute(cr, uid, product_currency,
336
move_currency_id, product_price)
337
new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
339
if product.qty_available <= 0:
340
new_std_price = new_price
342
# Get the standard price
343
amount_unit = product.price_get('standard_price', context)[product.id]
344
# check no division by zero
345
if product_avail[product.id] + qty:
346
new_std_price = ((amount_unit * product_avail[product.id])\
347
+ (new_price * qty))/(product_avail[product.id] + qty)
351
# Write the field according to price type field
352
product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
354
# Record the values that were chosen in the wizard, so they can be
355
# used for inventory valuation if real-time valuation is enabled.
356
average_values = {'price_unit': product_price,
357
'price_currency_id': product_currency}
359
count = count + partial['product_qty']
362
# update existing move
363
values = {'name': partial['name'],
364
'product_id': partial['product_id'],
365
'product_qty': partial['product_qty'],
366
'product_uos_qty': partial['product_qty'],
367
'prodlot_id': partial['prodlot_id'],
368
'product_uom': partial['product_uom'],
369
'asset_id': partial['asset_id'],
370
'change_reason': partial['change_reason'],
372
# average computation - empty if not average
373
values.update(average_values)
374
values = self._do_incoming_shipment_first_hook(cr, uid, ids, context, values=values)
375
move_obj.write(cr, uid, [move.id], values, context=context)
376
done_moves.append(move.id)
377
# if split happened, we update the corresponding OUT move
380
move_obj.write(cr, uid, [out_move_id], values, context=context)
381
elif move.product_id.id != partial['product_id']:
382
# no split but product changed, we have to update the corresponding out move
383
move_obj.write(cr, uid, [out_move_id], values, context=context)
384
# we force update flag - out will be updated if qty is missing - possibly with the creation of a new move
387
# split happened during the validation
388
# copy the stock move and set the quantity
389
values = {'name': partial['name'],
390
'product_id': partial['product_id'],
391
'product_qty': partial['product_qty'],
392
'product_uos_qty': partial['product_qty'],
393
'prodlot_id': partial['prodlot_id'],
394
'product_uom': partial['product_uom'],
395
'asset_id': partial['asset_id'],
396
'change_reason': partial['change_reason'],
399
# average computation - empty if not average
400
values.update(average_values)
401
new_move = move_obj.copy(cr, uid, move.id, values, context=context)
402
done_moves.append(new_move)
404
new_out_move = move_obj.copy(cr, uid, out_move_id, values, context=context)
405
# decrement the initial move, cannot be less than zero
406
diff_qty = initial_qty - count
407
# the quantity after the process does not correspond to the incoming shipment quantity
408
# the difference is written back to incoming shipment - and possibilty to OUT if split happened
409
# is positive if some qty was removed during the process -> current incoming qty is modified
410
# create a backorder if does not exist, copy original move with difference qty in it # DOUBLE CHECK ORIGINAL FUNCTION BEHAVIOR !!!!!
411
# if split happened, update the corresponding out move with diff_qty
414
# create the backorder - with no lines
415
backorder_id = self.copy(cr, uid, pick.id, {'name': sequence_obj.get(cr, uid, 'stock.picking.%s'%(pick.type)),
419
# create the corresponding move in the backorder - reset productionlot
420
defaults = {'name': data_back['name'],
421
'product_id': data_back['product_id'],
422
'product_uom': data_back['product_uom'],
423
'product_qty': diff_qty,
424
'product_uos_qty': diff_qty,
425
'picking_id': pick.id, # put in the current picking which will be the actual backorder (OpenERP logic)
428
'move_dest_id': False,
429
'price_unit': move.price_unit,
430
'change_reason': False,
432
# average computation - empty if not average
433
defaults.update(average_values)
434
new_back_move = move_obj.copy(cr, uid, move.id, defaults, context=context)
437
# update out move - quantity is increased, to match the original qty
438
self._update_mirror_move(cr, uid, ids, data_back, diff_qty, out_move=out_move_id, context=context)
439
# is negative if some qty was added during the validation -> draft qty is increased
441
# we update the corresponding OUT object if exists - we want to increase the qty if no split happened
442
# if split happened and quantity is bigger, the quantities are already updated with stock moves creation
444
update_qty = -diff_qty
445
self._update_mirror_move(cr, uid, ids, data_back, update_qty, out_move=out_move_id, context=context)
446
# clean the picking object - removing lines with 0 qty - force unlink
447
# this should not be a problem as IN moves are not referenced by other objects, only OUT moves are referenced
448
for move in pick.move_lines:
449
if not move.product_qty and move.state not in ('done', 'cancel'):
450
done_moves.remove(move.id)
451
move.unlink(context=dict(context, call_unlink=True))
452
# At first we confirm the new picking (if necessary) - **corrected** inverse openERP logic !
454
# done moves go to new picking object
455
move_obj.write(cr, uid, done_moves, {'picking_id': backorder_id}, context=context)
456
wf_service.trg_validate(uid, 'stock.picking', backorder_id, 'button_confirm', cr)
457
# Then we finish the good picking
458
self.write(cr, uid, [pick.id], {'backorder_id': backorder_id}, context=context)
459
self.action_move(cr, uid, [backorder_id])
460
wf_service.trg_validate(uid, 'stock.picking', backorder_id, 'button_done', cr)
461
wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
463
self.action_move(cr, uid, [pick.id])
464
wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
466
return {'type': 'ir.actions.act_window_close'}
468
def enter_reason(self, cr, uid, ids, context=None):
472
# we need the context for the wizard switch
476
name = _("Enter a Reason for Incoming cancellation")
477
model = 'enter.reason'
479
wiz_obj = self.pool.get('wizard')
480
# open the selected wizard
481
return wiz_obj.open_wizard(cr, uid, ids, name=name, model=model, step=step, context=dict(context, picking_id=ids[0]))
483
def cancel_and_update_out(self, cr, uid, ids, context=None):
485
update corresponding out picking if exists and cancel the picking
489
if isinstance(ids, (int, long)):
493
move_obj = self.pool.get('stock.move')
494
purchase_obj = self.pool.get('purchase.order')
496
wf_service = netsvc.LocalService("workflow")
498
for obj in self.browse(cr, uid, ids, context=context):
499
# corresponding sale ids to be manually corrected after purchase workflow trigger
501
for move in obj.move_lines:
502
data_back = self.create_data_back(cr, uid, move, context=context)
503
diff_qty = -data_back['product_qty']
504
# update corresponding out move
505
out_move_id = self._update_mirror_move(cr, uid, ids, data_back, diff_qty, out_move=False, context=context)
506
# for out cancellation, two points:
507
# - if pick/pack/ship: check that nothing is in progress
508
# - if nothing in progress, and the out picking is canceled, trigger the so to correct the corresponding so manually
510
out_move = move_obj.browse(cr, uid, out_move_id, context=context)
511
cond1 = out_move.picking_id.subtype == 'standard'
512
cond2 = out_move.picking_id.subtype == 'picking' and not out_move.picking_id.has_picking_ticket_in_progress(context=context)[out_move.picking_id.id]
513
if (cond1 or cond2) and out_move.picking_id.type == 'out' and not out_move.product_qty:
514
# the corresponding move can be canceled - the OUT picking workflow is triggered automatically if needed
515
move_obj.action_cancel(cr, uid, [out_move_id], context=context)
517
# - when searching for open picking tickets - we should take into account the specific move (only product id ?)
518
# - and also the state of the move not in (cancel done)
519
# correct the corresponding so manually if exists - could be in shipping exception
520
if out_move.picking_id and out_move.picking_id.sale_id:
521
if out_move.picking_id.sale_id.id not in sale_ids:
522
sale_ids.append(out_move.picking_id.sale_id.id)
524
# correct the corresponding po manually if exists - should be in shipping exception
526
wf_service.trg_validate(uid, 'purchase.order', obj.purchase_id.id, 'picking_ok', cr)
527
purchase_obj.log(cr, uid, obj.purchase_id.id, _('The Purchase Order %s is %s%% received.')%(obj.purchase_id.name, round(obj.purchase_id.shipped_rate,2)))
528
# correct the corresponding so
529
for sale_id in sale_ids:
530
wf_service.trg_validate(uid, 'sale.order', sale_id, 'ship_corrected', cr)
537
class purchase_order_line(osv.osv):
539
add the link to procurement order
541
_inherit = 'purchase.order.line'
542
_columns= {'procurement_id': fields.many2one('procurement.order', string='Procurement Reference', readonly=True,),
544
_defaults = {'procurement_id': False,}
547
purchase_order_line()
550
class procurement_order(osv.osv):
552
inherit po_values_hook
554
_inherit = 'procurement.order'
556
def po_line_values_hook(self, cr, uid, ids, context=None, *args, **kwargs):
558
Please copy this to your module's method also.
559
This hook belongs to the make_po method from purchase>purchase.py>procurement_order
561
- allow to modify the data for purchase order line creation
563
if isinstance(ids, (int, long)):
565
line = super(procurement_order, self).po_line_values_hook(cr, uid, ids, context=context, *args, **kwargs)
566
# give the purchase order line a link to corresponding procurement
567
procurement = kwargs['procurement']
568
line.update({'procurement_id': procurement.id,})