1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 datetime import date, datetime
23 from dateutil import relativedelta
27 from openerp.osv import fields, osv
28 from openerp.tools.translate import _
29 from openerp import tools
30 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
31 from openerp import SUPERUSER_ID
32 import openerp.addons.decimal_precision as dp
34 _logger = logging.getLogger(__name__)
37 #----------------------------------------------------------
39 #----------------------------------------------------------
40 class stock_incoterms(osv.osv):
41 _name = "stock.incoterms"
42 _description = "Incoterms"
44 'name': fields.char('Name', size=64, required=True, help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices."),
45 'code': fields.char('Code', size=3, required=True, help="Incoterm Standard Code"),
46 'active': fields.boolean('Active', help="By unchecking the active field, you may hide an INCOTERM you will not use."),
52 #----------------------------------------------------------
54 #----------------------------------------------------------
56 class stock_location(osv.osv):
57 _name = "stock.location"
58 _description = "Inventory Locations"
59 _parent_name = "location_id"
61 _parent_order = 'name'
62 _order = 'parent_left'
63 _rec_name = 'complete_name'
65 def _complete_name(self, cr, uid, ids, name, args, context=None):
66 """ Forms complete name of location from parent location to child location.
67 @return: Dictionary of values
70 for m in self.browse(cr, uid, ids, context=context):
72 parent = m.location_id
74 res[m.id] = parent.name + ' / ' + res[m.id]
75 parent = parent.location_id
78 def _get_sublocations(self, cr, uid, ids, context=None):
79 """ return all sublocations of the given stock locations (included) """
82 context_with_inactive = context.copy()
83 context_with_inactive['active_test']=False
84 return self.search(cr, uid, [('id', 'child_of', ids)], context=context_with_inactive)
87 'name': fields.char('Location Name', size=64, required=True, translate=True),
88 'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
89 'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Type', required=True,
90 help="""* Supplier Location: Virtual location representing the source location for products coming from your suppliers
91 \n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products
92 \n* Internal Location: Physical locations inside your own warehouses,
93 \n* Customer Location: Virtual location representing the destination location for products sent to your customers
94 \n* Inventory: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)
95 \n* Procurement: Virtual location serving as temporary counterpart for procurement operations when the source (supplier or production) is not known yet. This location should be empty when the procurement scheduler has finished running.
96 \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
99 'complete_name': fields.function(_complete_name, type='char', string="Location Name",
100 store={'stock.location': (_get_sublocations, ['name', 'location_id', 'active'], 10)}),
101 'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
102 'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
104 'partner_id': fields.many2one('res.partner', 'Owner', help="Owner of the location if not internal"),
106 'comment': fields.text('Additional Information'),
107 'posx': fields.integer('Corridor (X)', help="Optional localization details, for information purpose only"),
108 'posy': fields.integer('Shelves (Y)', help="Optional localization details, for information purpose only"),
109 'posz': fields.integer('Height (Z)', help="Optional localization details, for information purpose only"),
111 'parent_left': fields.integer('Left Parent', select=1),
112 'parent_right': fields.integer('Right Parent', select=1),
114 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between all companies'),
115 'scrap_location': fields.boolean('Scrap Location', help='Check this box to allow using this location to put scrapped/damaged goods.'),
116 'removal_strategy_ids': fields.one2many('product.removal', 'location_id', 'Removal Strategies'),
117 'putaway_strategy_ids': fields.one2many('product.putaway', 'location_id', 'Put Away Strategies'),
122 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
126 'scrap_location': False,
129 def get_putaway_strategy(self, cr, uid, location, product, context=None):
130 pa = self.pool.get('product.putaway')
131 categ = product.categ_id
132 categs = [categ.id, False]
133 while categ.parent_id:
134 categ = categ.parent_id
135 categs.append(categ.id)
137 result = pa.search(cr,uid, [
138 ('location_id', '=', location.id),
139 ('product_categ_id', 'in', categs)
142 return pa.browse(cr, uid, result[0], context=context)
144 def get_removal_strategy(self, cr, uid, location, product, context=None):
145 pr = self.pool.get('product.removal')
146 categ = product.categ_id
147 categs = [categ.id, False]
148 while categ.parent_id:
149 categ = categ.parent_id
150 categs.append(categ.id)
152 result = pr.search(cr,uid, [
153 ('location_id', '=', location.id),
154 ('product_categ_id', 'in', categs)
157 return pr.browse(cr, uid, result[0], context=context).method
160 #----------------------------------------------------------
162 #----------------------------------------------------------
164 class stock_location_route(osv.osv):
165 _name = 'stock.location.route'
166 _description = "Inventory Routes"
170 'name': fields.char('Route Name', required=True),
171 'sequence': fields.integer('Sequence'),
172 'pull_ids': fields.one2many('procurement.rule', 'route_id', 'Pull Rules'),
173 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it."),
174 'push_ids': fields.one2many('stock.location.path', 'route_id', 'Push Rules'),
175 'product_selectable': fields.boolean('Selectable on Product'),
176 'product_categ_selectable': fields.boolean('Selectable on Product Category'),
177 'warehouse_selectable': fields.boolean('Selectable on Warehouse'),
178 'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'),
179 'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'),
183 'sequence': lambda self, cr, uid, ctx: 0,
185 'product_selectable': True,
190 #----------------------------------------------------------
192 #----------------------------------------------------------
194 class stock_quant(osv.osv):
196 Quants are the smallest unit of stock physical instances
198 _name = "stock.quant"
199 _description = "Quants"
201 def _get_quant_name(self, cr, uid, ids, name, args, context=None):
202 """ Forms complete name of location from parent location to child location.
203 @return: Dictionary of values
206 for q in self.browse(cr, uid, ids, context=context):
208 res[q.id] = q.product_id.code or ''
210 res[q.id] = q.lot_id.name
211 res[q.id] += ': '+ str(q.qty) + q.product_id.uom_id.name
215 'name': fields.function(_get_quant_name, type='char', string='Identifier'),
216 'product_id': fields.many2one('product.product', 'Product', required=True),
217 'location_id': fields.many2one('stock.location', 'Location', required=True),
218 'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product"),
219 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant"),
220 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', store=True),
221 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for"),
222 'reservation_op_id': fields.many2one('stock.pack.operation', 'Reserved for Pack Operation', help="The operation the quant is reserved for"),
223 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
224 'cost': fields.float('Unit Cost'),
225 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant"),
227 'create_date': fields.datetime('Creation Date'),
228 'in_date': fields.datetime('Incoming Date'),
230 'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
231 'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True),
233 # Used for negative quants to reconcile after compensated by a new positive one
234 'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from'),
235 'negative_dest_location_id': fields.many2one('stock.location', 'Destination Location', help='Technical field used to record the destination location of a move that created a negative quant'),
239 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.quant', context=c),
242 def quants_reserve(self, cr, uid, quants, move, context=None):
244 for quant,qty in quants:
245 if not quant: continue
246 self._quant_split(cr, uid, quant, qty, context=context)
247 toreserve.append(quant.id)
248 return self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id}, context=context)
250 # add location_dest_id in parameters (False=use the destination of the move)
251 def quants_move(self, cr, uid, quants, move, lot_id = False, owner_id = False, package_id = False, context=None):
252 for quant, qty in quants:
253 #quant may be a browse record or None
254 quant_record = self.move_single_quant(cr, uid, quant, qty, move, lot_id = lot_id, package_id = package_id, context=context)
255 #quant_record is the quant newly created or already split
256 self._quant_reconcile_negative(cr, uid, quant_record, context=context)
259 def check_preferred_location(self, cr, uid, move, context=None):
260 if move.putaway_ids and move.putaway_ids[0]:
261 #Take only first suggestion for the moment
262 return move.putaway_ids[0].location_id
263 return move.location_dest_id
265 def move_single_quant(self, cr, uid, quant, qty, move, lot_id = False, owner_id = False, package_id = False, context=None):
267 quant = self._quant_create(cr, uid, qty, move, lot_id = lot_id, owner_id = owner_id, package_id = package_id, context = context)
269 self._quant_split(cr, uid, quant, qty, context=context)
270 # FP Note: improve this using preferred locations
271 location_to = self.check_preferred_location(cr, uid, move, context=context)
272 self.write(cr, SUPERUSER_ID, [quant.id], {
273 'location_id': location_to.id,
274 'history_ids': [(4, move.id)]
279 def quants_get_prefered_domain(self, cr, uid, location, product, qty, domain=None, prefered_domain=False, fallback_domain=False, restrict_lot_id=False, restrict_partner_id=False, context=None):
280 ''' This function tries to find quants in the given location for the given domain, by trying to first limit
281 the choice on the quants that match the prefered_domain as well. But if the qty requested is not reached
282 it tries to find the remaining quantity by using the fallback_domain.
284 if prefered_domain and fallback_domain:
287 quants = self.quants_get(cr, uid, location, product, qty, domain=domain + prefered_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
292 quant_ids.append(quant[0].id)
295 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
297 #make sure the quants aren't found twice (if the prefered_domain and the fallback_domain aren't orthogonal
298 domain += [('id', 'not in', quant_ids)]
299 unprefered_quants = self.quants_get(cr, uid, location, product, res_qty, domain=domain + fallback_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
300 for quant in unprefered_quants:
303 return self.quants_get(cr, uid, location, product, qty, domain=domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
305 def quants_get(self, cr, uid, location, product, qty, domain=None, restrict_lot_id=False, restrict_partner_id=False, context=None):
307 Use the removal strategies of product to search for the correct quants
308 If you inherit, put the super at the end of your method.
310 :location: browse record of the parent location in which the quants have to be found
311 :product: browse record of the product to find
312 :qty in UoM of product
315 domain = domain or [('qty', '>', 0.0)]
316 if restrict_partner_id:
317 domain += [('owner_id', '=', restrict_partner_id)]
319 domain += [('lot_id', '=', restrict_lot_id)]
321 removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) or 'fifo'
322 if removal_strategy == 'fifo':
323 result += self._quants_get_fifo(cr, uid, location, product, qty, domain, context=context)
324 elif removal_strategy == 'lifo':
325 result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context)
327 raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,)))
330 def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, package_id = False, force_location=False, context=None):
331 '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location.
335 price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
336 location = force_location or move.location_dest_id
338 'product_id': move.product_id.id,
339 'location_id': location.id,
342 'history_ids': [(4, move.id)],
343 'in_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
344 'company_id': move.company_id.id,
346 'owner_id': owner_id,
349 if move.location_id.usage == 'internal':
350 #if we were trying to move something from an internal location and reach here (quant creation),
351 #it means that a negative quant has to be created as well.
352 negative_vals = vals.copy()
353 negative_vals['location_id'] = move.location_id.id
354 negative_vals['qty'] = -qty
355 negative_vals['cost'] = price_unit
356 negative_vals['negative_dest_location_id'] = move.location_dest_id.id
357 negative_vals['package_id'] = package_id
358 negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
359 vals.update({'propagated_from_id': negative_quant_id})
361 #create the quant as superuser, because we want to restrict the creation of quant manually: they should always use this method to create quants
362 quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
363 return self.browse(cr, uid, quant_id, context=context)
365 def _quant_split(self, cr, uid, quant, qty, context=None):
366 context = context or {}
367 if (quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty):
369 new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty}, context=context)
370 self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context)
372 return self.browse(cr, uid, new_quant, context=context)
374 def _get_latest_move(self, cr, uid, quant, context=None):
376 for m in quant.history_ids:
377 if not move or m.date > move.date:
381 #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
382 # move = self._get_latest_move(cr, uid, to_solve_quant, context=context)
383 # remaining_solving_quant = self._quant_split(cr, uid, quant, qty, context=context)
384 # remaining_to_solve_quant = self._quant_split(cr, uid, to_solve_quant, qty, context=context)
385 # remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
386 # #if the reconciliation was not complete, we need to link together the remaining parts
387 # if remaining_to_solve_quant and remaining_neg_quant:
388 # self.write(cr, uid, remaining_to_solve_quant.id, {'propagated_from_id': remaining_neg_quant.id}, context=context)
389 # #delete the reconciled quants, as it is replaced by the solving quant
390 # if remaining_neg_quant:
391 # otherquant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
392 # self.write(cr, uid, otherquant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
393 # self.unlink(cr, SUPERUSER_ID, [quant_neg.id, to_solve_quant.id], context=context)
394 # #call move_single_quant to ensure recursivity if necessary and do the stock valuation
395 # self.move_single_quant(cr, uid, quant, qty, move, context=context)
396 # return remaining_solving_quant, remaining_to_solve_quant
398 def _quants_merge(self, cr, uid, solved_quant_ids, solving_quant, context=None):
400 for move in solving_quant.history_ids:
401 path.append((4, move.id))
402 self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
404 def _quant_reconcile_negative(self, cr, uid, quant, context=None):
406 When new quant arrive in a location, try to reconcile it with
407 negative quants. If it's possible, apply the cost of the new
408 quant to the conter-part of the negative quant.
410 if quant.location_id.usage != 'internal':
412 solving_quant = quant
413 dom = [('qty', '<', 0)]
414 dom += [('lot_id', '=', quant.lot_id and quant.lot_id.id or False)]
415 dom += [('owner_id', '=', quant.owner_id and quant.owner_id.id or False)]
416 dom += [('package_id', '=', quant.package_id and quant.package_id.id or False)]
417 quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, [('qty', '<', '0')], context=context)
418 for quant_neg, qty in quants:
421 to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
422 if not to_solve_quant_ids:
425 solved_quant_ids = []
426 for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
429 solved_quant_ids.append(to_solve_quant.id)
430 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
431 solving_qty -= min(solving_qty, to_solve_quant.qty)
432 remaining_solving_quant = self._quant_split(cr, uid, solving_quant, qty, context=context)
433 remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
434 #if the reconciliation was not complete, we need to link together the remaining parts
435 if remaining_neg_quant:
436 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
437 if remaining_to_solve_quant_ids:
438 self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
439 #delete the reconciled quants, as it is replaced by the solved quants
440 self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
441 #price update + accounting entries adjustments
442 self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
443 #merge history (and cost?)
444 self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
445 self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
446 solving_quant = remaining_solving_quant
448 #solving_quant, dummy = self._reconcile_single_negative_quant(cr, uid, to_solve_quant, solving_quant, quant_neg, qty, context=context)
450 def _price_update(self, cr, uid, ids, newprice, context=None):
451 self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context)
453 def write(self, cr, uid, ids, vals, context=None):
454 #We want to trigger the move with nothing on reserved_quant_ids for the store of the remaining quantity
455 if 'reservation_id' in vals:
456 reservation_ids = self.browse(cr, uid, ids, context=context)
457 moves_to_warn = set()
458 for reser in reservation_ids:
459 if reser.reservation_id:
460 moves_to_warn.add(reser.reservation_id.id)
461 self.pool.get('stock.move').write(cr, uid, list(moves_to_warn), {'reserved_quant_ids': []}, context=context)
462 return super(stock_quant, self).write(cr, SUPERUSER_ID, ids, vals, context=context)
464 def quants_unreserve(self, cr, uid, move, context=None):
465 related_quants = [x.id for x in move.reserved_quant_ids]
466 return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'reservation_op_id': False}, context=context)
468 def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
469 ''' Implementation of removal strategies
470 If it can not reserve, it will return a tuple (None, qty)
472 domain += location and [('location_id', 'child_of', location.id)] or []
473 domain += [('product_id', '=', product.id)] + domain
477 quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
479 res.append((None, quantity))
481 for quant in self.browse(cr, uid, quants, context=context):
482 if quantity >= abs(quant.qty):
483 res += [(quant, abs(quant.qty))]
484 quantity -= abs(quant.qty)
486 res += [(quant, quantity)]
492 def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None):
494 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
496 def _quants_get_lifo(self, cr, uid, location, product, quantity, domain=[], context=None):
497 order = 'in_date desc'
498 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
500 def _location_owner(self, cr, uid, quant, location, context=None):
501 ''' Return the company owning the location if any '''
502 return location and (location.usage == 'internal') and location.company_id or False
504 def _check_location(self, cr, uid, ids, context=None):
505 for record in self.browse(cr, uid, ids, context=context):
506 if record.location_id.usage == 'view':
507 raise osv.except_osv(_('Error'), _('You cannot move product %s to a location of type view %s.')% (record.product_id.name, record.location_id.name))
511 (_check_location, 'You cannot move products to a location of the type view.', ['location_id'])
515 #----------------------------------------------------------
517 #----------------------------------------------------------
519 class stock_picking(osv.osv):
520 _name = "stock.picking"
521 _inherit = ['mail.thread']
522 _description = "Picking List"
523 _order = "priority desc, date desc, id desc"
525 def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
526 move_obj = self.pool.get("stock.move")
528 move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
529 move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
531 def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
532 """ Finds minimum and maximum dates for picking.
533 @return: Dictionary of values
537 res[id] = {'min_date': False, 'max_date': False}
549 picking_id""",(tuple(ids),))
550 for pick, dt1, dt2 in cr.fetchall():
551 res[pick]['min_date'] = dt1
552 res[pick]['max_date'] = dt2
555 def create(self, cr, user, vals, context=None):
556 context = context or {}
557 if ('name' not in vals) or (vals.get('name') in ('/', False)):
558 ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
559 sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
560 vals['name'] = self.pool.get('ir.sequence').get_id(cr, user, sequence_id, 'id', context=context)
562 return super(stock_picking, self).create(cr, user, vals, context)
564 def _state_get(self, cr, uid, ids, field_name, arg, context=None):
565 '''The state of a picking depends on the state of its related stock.move
566 draft: the picking has no line or any one of the lines is draft
567 done, draft, cancel: all lines are done / draft / cancel
568 confirmed, auto, assigned depends on move_type (all at once or direct)
571 for pick in self.browse(cr, uid, ids, context=context):
572 if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]):
573 res[pick.id] = 'draft'
575 if all([x.state == 'cancel' for x in pick.move_lines]):
576 res[pick.id] = 'cancel'
578 if all([x.state in ('cancel','done') for x in pick.move_lines]):
579 res[pick.id] = 'done'
582 order = {'confirmed':0, 'waiting':1, 'assigned':2}
583 order_inv = dict(zip(order.values(),order.keys()))
584 lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel','done')]
585 if pick.move_lines == 'one':
586 res[pick.id] = order_inv[min(lst)]
588 res[pick.id] = order_inv[max(lst)]
591 def _get_pickings(self, cr, uid, ids, context=None):
593 for move in self.browse(cr, uid, ids, context=context):
595 res.add(move.picking_id.id)
598 def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
600 for pick in self.browse(cr, uid, ids, context=context):
602 if pick.pack_operation_ids:
606 def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
608 for pick in self.browse(cr, uid, ids, context=context):
610 for move in pick.move_lines:
611 if move.reserved_quant_ids:
616 def action_assign_owner(self, cr, uid, ids, context=None):
617 for picking in self.browse(cr, uid, ids, context=context):
618 packop_ids = [op.id for op in picking.pack_operation_ids]
619 self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
622 'name': fields.char('Reference', size=64, select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
623 'origin': fields.char('Source Document', size=64, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="Reference of the document", select=True),
624 'backorder_id': fields.many2one('stock.picking', 'Back Order of', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.", select=True),
625 'note': fields.text('Notes', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
626 'move_type': fields.selection([('direct', 'Partial'), ('one', 'All at once')], 'Delivery Method', required=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="It specifies goods to be deliver partially or all at once"),
627 'state': fields.function(_state_get, type="selection", store = {
628 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'move_lines'], 20),
629 'stock.move': (_get_pickings, ['state', 'picking_id'], 20)}, selection = [
631 ('cancel', 'Cancelled'),
632 ('waiting', 'Waiting Another Operation'),
633 ('confirmed', 'Waiting Availability'),
634 ('assigned', 'Ready to Transfer'),
635 ('done', 'Transferred'),
636 ], string='Status', readonly=True, select=True, track_visibility='onchange', help="""
637 * Draft: not confirmed yet and will not be scheduled until confirmed\n
638 * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
639 * Waiting Availability: still waiting for the availability of products\n
640 * Ready to Transfer: products reserved, simply waiting for confirmation.\n
641 * Transferred: has been processed, can't be modified or cancelled anymore\n
642 * Cancelled: has been cancelled, can't be confirmed anymore"""
644 'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], string='Priority', required=True),
645 'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
646 store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed"),
647 'max_date': fields.function(get_min_max_date, multi="min_max_date",
648 store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
649 'date': fields.datetime('Commitment Date', help="Date promised for the completion of the transfer order, usually set the time of the order and revised later on.", select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
650 'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
651 'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
652 'quant_reserved_exist': fields.function(_get_quant_reserved_exist, type='boolean', string='Quant already reserved ?', help='technical field used to know if there is already at least one quant reserved on moves of a given picking'),
653 'partner_id': fields.many2one('res.partner', 'Partner', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
654 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
655 'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', string='Related Packing Operations'),
656 'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
657 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', required=True),
659 'owner_id': fields.many2one('res.partner', 'Owner', help="Default Owner"),
660 # Used to search on pickings
661 'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),#?
662 'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
663 'location_dest_id': fields.related('move_lines', 'location_dest_id', type='many2one', relation='stock.location', string='Destination Location', readonly=True),
664 'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True,
666 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
667 'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
672 'name': lambda self, cr, uid, context: '/',
675 'priority' : '1', #normal
676 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
677 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c)
680 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
683 def copy(self, cr, uid, id, default=None, context=None):
686 default = default.copy()
687 picking_obj = self.browse(cr, uid, id, context=context)
688 if ('name' not in default) or (picking_obj.name == '/'):
689 default['name'] = '/'
690 if not default.get('backorder_id'):
691 default['backorder_id'] = False
693 return super(stock_picking, self).copy(cr, uid, id, default, context)
696 def action_confirm(self, cr, uid, ids, context=None):
698 todo_force_assign = []
700 for picking in self.browse(cr, uid, ids, context=context):
701 if picking.picking_type_id.auto_force_assign:
702 todo_force_assign.append(picking.id)
703 for r in picking.move_lines:
704 if r.state == 'draft':
707 self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
709 if todo_force_assign:
710 self.force_assign(cr, uid, todo_force_assign, context=context)
714 def action_assign(self, cr, uid, ids, *args):
715 """ Changes state of picking to available if all moves are confirmed.
718 for pick in self.browse(cr, uid, ids):
719 if pick.state == 'draft':
720 self.action_confirm(cr, uid, [pick.id])
721 move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
723 raise osv.except_osv(_('Warning!'), _('No product available.'))
724 self.pool.get('stock.move').action_assign(cr, uid, move_ids)
727 def force_assign(self, cr, uid, ids, context=None):
728 """ Changes state of picking to available if moves are confirmed or waiting.
731 for pick in self.browse(cr, uid, ids, context=context):
732 move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
733 self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
736 def cancel_assign(self, cr, uid, ids, *args):
737 """ Cancels picking and moves.
740 for pick in self.browse(cr, uid, ids):
741 move_ids = [x.id for x in pick.move_lines]
742 self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
745 def action_cancel(self, cr, uid, ids, context=None):
746 for pick in self.browse(cr, uid, ids, context=context):
747 ids2 = [move.id for move in pick.move_lines]
748 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
751 def action_done(self, cr, uid, ids, context=None):
752 """Changes picking state to done by processing the Stock Moves of the Picking
754 Normally that happens when the button "Done" is pressed on a Picking view.
757 for pick in self.browse(cr, uid, ids, context=context):
759 for move in pick.move_lines:
760 if move.state == 'draft':
761 self.pool.get('stock.move').action_confirm(cr, uid, [move.id],
764 elif move.state in ('assigned','confirmed'):
767 self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
770 def unlink(self, cr, uid, ids, context=None):
771 move_obj = self.pool.get('stock.move')
772 context = context or {}
773 for pick in self.browse(cr, uid, ids, context=context):
774 ids2 = [move.id for move in pick.move_lines]
775 move_obj.action_cancel(cr, uid, ids2, context=context)
776 move_obj.unlink(cr, uid, ids2, context=context)
777 return super(stock_picking, self).unlink(cr, uid, ids, context=context)
779 # Methods for partial pickings
781 def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
783 Move all non-done lines into a new backorder picking
785 if not backorder_moves:
786 backorder_moves = picking.move_lines
787 backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done','cancel')]
788 if 'do_only_split' in context and context['do_only_split']:
789 backorder_move_ids = [x.id for x in backorder_moves if x.id not in context['split']]
791 if backorder_move_ids:
792 backorder_id = self.copy(cr, uid, picking.id, {
795 'pack_operation_ids': [],
796 'backorder_id': picking.id,
798 back_order_name = self.browse(cr, uid, backorder_id, context=context).name
799 self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (back_order_name), context=context)
800 move_obj = self.pool.get("stock.move")
801 move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
803 self.pool.get("stock.picking").action_confirm(cr, uid, [picking.id], context=context)
804 self.action_confirm(cr, uid, [backorder_id], context=context)
808 def do_prepare_partial(self, cr, uid, picking_ids, context=None):
809 context = context or {}
810 pack_operation_obj = self.pool.get('stock.pack.operation')
811 pack_obj = self.pool.get("stock.quant.package")
812 quant_obj = self.pool.get("stock.quant")
813 for picking in self.browse(cr, uid, picking_ids, context=context):
814 for move in picking.move_lines:
815 if move.state != 'assigned': continue
816 #Check which of the reserved quants are entirely in packages (can be in separate method)
817 packages = list(set([x.package_id for x in move.reserved_quant_ids if x.package_id]))
819 for pack in packages:
824 quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
825 if all([x.reservation_id.id == move.id for x in quant_obj.browse(cr, uid, quants, context=context) if x.reservation_id]):
826 good_pack = test_pack.id
827 if test_pack.parent_id:
828 test_pack = test_pack.parent_id
834 done_packages.append(good_pack)
835 done_packages = list(set(done_packages))
837 #Create package operations
838 reserved = set([x.id for x in move.reserved_quant_ids])
839 remaining_qty = move.product_qty
840 for pack in pack_obj.browse(cr, uid, done_packages, context=context):
841 quantl = pack_obj.get_content(cr, uid, [pack.id], context=context)
842 for quant in quant_obj.browse(cr, uid, quantl, context=context):
843 remaining_qty -= quant.qty
844 quants = set(pack_obj.get_content(cr, uid, [pack.id], context=context))
846 pack_operation_obj.create(cr, uid, {
847 'picking_id': picking.id,
848 'package_id': pack.id,
852 yet_to_reserve = list(reserved)
853 #Create operations based on quants
854 for quant in quant_obj.browse(cr, uid, yet_to_reserve, context=context):
855 qty = min(quant.qty, move.product_qty)
857 pack_operation_obj.create(cr, uid, {
858 'picking_id': picking.id,
860 'quant_id': quant.id,
861 'product_id': quant.product_id.id,
862 'lot_id': quant.lot_id and quant.lot_id.id or False,
863 'product_uom_id': quant.product_id.uom_id.id,
864 'owner_id': quant.owner_id and quant.owner_id.id or False,
866 'package_id': quant.package_id and quant.package_id.id or False,
868 if remaining_qty > 0:
869 pack_operation_obj.create(cr, uid, {
870 'picking_id': picking.id,
871 'product_qty': remaining_qty,
872 'product_id': move.product_id.id,
873 'product_uom_id': move.product_id.uom_id.id,
874 'cost': move.product_id.standard_price,
878 def do_rereserve(self, cr, uid, picking_ids, context=None):
880 Needed for parameter create
882 self.rereserve(cr, uid, picking_ids, context=context)
884 def do_unreserve(self,cr,uid,picking_ids, context=None):
886 Will remove all quants for picking in picking_ids
889 quant_obj = self.pool.get("stock.quant")
890 for picking in self.browse(cr, uid, picking_ids, context=context):
891 for move in picking.move_lines:
892 ids_to_free += [quant.id for quant in move.reserved_quant_ids]
894 quant_obj.write(cr, SUPERUSER_ID, ids_to_free, {'reservation_id' : False, 'reservation_op_id': False }, context = context)
896 def _reserve_quants_ops_move(self, cr, uid, ops, move, qty, create=False, context=None):
898 Will return the quantity that could not be reserved
900 quant_obj = self.pool.get("stock.quant")
901 op_obj = self.pool.get("stock.pack.operation")
902 if create and move.location_id.usage != 'internal':
904 quant = quant_obj._quant_create(cr, uid, qty, move, lot_id=ops.lot_id and ops.lot_id.id or False, owner_id=ops.owner_id and ops.owner_id.id or False, context=context)
905 #TODO: location_id -> force location?
906 quant.write({'reservation_op_id': ops.id, 'location_id': move.location_id.id})
907 quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context)
911 #prefered_order = "reservation_id IS NOT NULL" #TODO: reservation_id as such might work
912 dom = op_obj._get_domain(cr, uid, ops, context=context)
913 dom = dom + [('reservation_id', 'not in', [x.id for x in move.picking_id.move_lines])]
914 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_domain=[('reservation_id', '=', False)], fallback_domain=[('reservation_id', '!=', False)], context=context)
917 if quant[0]: # If quant can be reserved
919 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
920 quant_obj.write(cr, SUPERUSER_ID, [x[0].id for x in quants if x[0]], {'reservation_op_id': ops.id}, context=context)
923 def rereserve(self, cr, uid, picking_ids, create=False, context=None):
925 This will unreserve all products and reserve the quants from the operations again
926 :return: Tuple (res, res2, resneg)
927 res: dictionary of ops with quantity that could not be processed matching ops and moves
928 res2: dictionary of moves with quantity that could not be processed matching ops and moves
929 resneg: the negative quants to be created: resneg[move][ops] gives negative quant to be created
930 tuple of dictionary with quantities of quant operation and product that can not be matched between ops and moves
931 and dictionary with remaining values on moves
933 quant_obj = self.pool.get("stock.quant")
934 pack_obj = self.pool.get("stock.quant.package")
935 res = {} # Qty still to do from ops
936 res2 = {} #what is left from moves
937 resneg= {} #Number of negative quants to create for move/op
938 for picking in self.browse(cr, uid, picking_ids, context=context):
940 # unreserve everything and initialize res2
941 for move in picking.move_lines:
942 quant_obj.quants_unreserve(cr, uid, move, context=context)
943 res2[move.id] = move.product_qty
945 if move.state == 'assigned':
946 products_moves.setdefault(move.product_id.id, []).append(move)
949 # Resort pack_operation_ids such that package transfers happen first and then the most specific operations from the product
951 orderedpackops = picking.pack_operation_ids
952 orderedpackops.sort(key = lambda x: ((x.package_id and not x.product_id) and -3 or 0) + (x.package_id and -1 or 0) + (x.lot_id and -1 or 0))
954 for ops in orderedpackops:
955 #If a product is specified in the ops, search for appropriate quants
958 move_ids = ops.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[ops.product_id.id]) or []
959 qty_to_do = ops.product_qty
960 while qty_to_do > 0 and move_ids:
961 move = move_ids.pop()
962 if res2[move.id] > qty_to_do:
967 qty_to_do -= res2[move.id]
968 neg_qty = self._reserve_quants_ops_move(cr, uid, ops, move, qty, create=create, context=context)
970 resneg[move.id].setdefault(ops.id, 0)
971 resneg [move.id][ops.id] += neg_qty
974 res[ops.id][ops.product_id.id] = qty_to_do
975 # In case only a package is specified, take all the quants from the package
977 quants = quant_obj.browse(cr, uid, pack_obj.get_content(cr, uid, [ops.package_id.id], context=context))
978 quants = [x for x in quants if x.qty > 0] #Negative quants should not be moved
981 move_ids = quant.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[quant.product_id.id]) or []
982 qty_to_do = quant.qty
983 while qty_to_do > 0 and move_ids:
984 move = move_ids.pop()
985 if res2[move.id] > qty_to_do:
990 qty_to_do -= res2[move.id]
991 quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context)
992 quant_obj.write(cr, uid, [quant.id], {'reservation_op_id': ops.id}, context=context)
994 res.setdefault(ops.id, {}).setdefault(quant.product_id.id, 0.0)
995 res[ops.id][quant.product_id.id] += qty_to_do
996 return (res, res2, resneg)
999 def do_partial(self, cr, uid, picking_ids, context=None):
1001 If no pack operation, we do simple action_done of the picking
1002 Otherwise, do the pack operations
1006 stock_move_obj = self.pool.get('stock.move')
1007 for picking in self.browse(cr, uid, picking_ids, context=context):
1008 if not picking.pack_operation_ids:
1009 self.action_done(cr, uid, [picking.id], context=context)
1013 # TODO: quants could have been created already in Supplier, so create parameter could disappear
1014 res = self.rereserve(cr, uid, [picking.id], create = True, context = context) #This time, quants need to be created
1016 orig_moves = picking.move_lines
1018 for orig in orig_moves:
1019 orig_qtys[orig.id] = orig.product_qty
1020 #Add moves that operations need extra
1022 for ops in res[0].keys():
1023 for prod in res[0][ops].keys():
1024 product = self.pool.get('product.product').browse(cr, uid, prod, context=context)
1025 qty = res[0][ops][prod]
1027 #Create moves for products too many on operation
1028 move_id = stock_move_obj.create(cr, uid, {
1029 'name': product.name,
1030 'product_id': product.id,
1031 'product_uom_qty': qty,
1032 'product_uom': product.uom_id.id,
1033 'location_id': picking.location_id.id,
1034 'location_dest_id': picking.location_dest_id.id,
1035 'picking_id': picking.id,
1036 'picking_type_id': picking.picking_type_id.id,
1037 'group_id': picking.group_id.id,
1039 stock_move_obj.action_confirm(cr, uid, [move_id], context=context)
1040 move = stock_move_obj.browse(cr, uid, move_id, context=context)
1041 ops_rec = self.pool.get("stock.pack.operation").browse(cr, uid, ops, context=context)
1042 resneg[move_id] = {}
1043 resneg[move_id][ops] = self._reserve_quants_ops_move(cr, uid, ops_rec, move, qty, create=True, context=context)
1044 extra_moves.append(move_id)
1047 for move in res2.keys():
1049 mov = stock_move_obj.browse(cr, uid, move, context=context)
1050 new_move = stock_move_obj.split(cr, uid, mov, res2[move], context=context)
1051 #Assign move as it was assigned before
1052 stock_move_obj.action_assign(cr, uid, [new_move])
1054 orig_moves = [x for x in orig_moves if res[1][x.id] < orig_qtys[x.id]]
1055 for move in orig_moves + stock_move_obj.browse(cr, uid, extra_moves, context=context):
1056 if move.state == 'draft':
1057 self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context)
1058 todo.append(move.id)
1059 elif move.state in ('assigned','confirmed'):
1060 todo.append(move.id)
1061 if len(todo) and not ('do_only_split' in context and context['do_only_split']):
1062 self.pool.get('stock.move').action_done(cr, uid, todo, negatives = resneg, context=context)
1063 elif 'do_only_split' in context and context['do_only_split']:
1064 context.update({'split': [x.id for x in orig_moves] + extra_moves})
1066 self._create_backorder(cr, uid, picking, context=context)
1069 def do_split(self, cr, uid, picking_ids, context=None):
1071 just split the picking without making it 'done'
1075 ctx = context.copy()
1076 ctx['do_only_split'] = True
1077 self.do_partial(cr, uid, picking_ids, context=ctx)
1080 # Methods for the barcode UI
1082 def get_picking_for_packing_ui(self, cr, uid, context=None):
1083 return self.search(cr, uid, [('state', 'in', ('confirmed', 'assigned')), ('picking_type_id', '=', context.get('default_picking_type_id'))], context=context)
1085 def action_done_from_packing_ui(self, cr, uid, picking_id, only_split_lines=False, context=None):
1086 self.do_partial(cr, uid, picking_id, only_split_lines, context=context)
1087 #return id of next picking to work on
1088 return self.get_picking_for_packing_ui(cr, uid, context=context)
1090 def action_pack(self, cr, uid, picking_ids, context=None):
1091 stock_operation_obj = self.pool.get('stock.pack.operation')
1092 package_obj = self.pool.get('stock.quant.package')
1093 for picking_id in picking_ids:
1094 operation_ids = stock_operation_obj.search(cr, uid, [('picking_id', '=', picking_id), ('result_package_id', '=', False)], context=context)
1096 package_id = package_obj.create(cr, uid, {}, context=context)
1097 stock_operation_obj.write(cr, uid, operation_ids, {'result_package_id': package_id}, context=context)
1100 def _deal_with_quants(self, cr, uid, picking_id, quant_ids, context=None):
1101 stock_operation_obj = self.pool.get('stock.pack.operation')
1103 todo_on_operations = []
1104 for quant in self.pool.get('stock.quant').browse(cr, uid, quant_ids, context=context):
1105 tmp_moves, tmp_operations = stock_operation_obj._search_and_increment(cr, uid, picking_id, ('quant_id', '=', quant.id), context=context)
1106 todo_on_moves += tmp_moves
1107 todo_on_operations += tmp_operations
1108 return todo_on_moves, todo_on_operations
1110 def get_barcode_and_return_todo_stuff(self, cr, uid, picking_id, barcode_str, context=None):
1111 '''This function is called each time there barcode scanner reads an input'''
1112 #TODO: better error messages handling => why not real raised errors
1113 quant_obj = self.pool.get('stock.quant')
1114 package_obj = self.pool.get('stock.quant.package')
1115 product_obj = self.pool.get('product.product')
1116 stock_operation_obj = self.pool.get('stock.pack.operation')
1119 todo_on_operations = []
1120 #check if the barcode correspond to a product
1121 matching_product_ids = product_obj.search(cr, uid, [('ean13', '=', barcode_str)], context=context)
1122 if matching_product_ids:
1123 todo_on_moves, todo_on_operations = stock_operation_obj._search_and_increment(cr, uid, picking_id, ('product_id', '=', matching_product_ids[0]), context=context)
1125 #check if the barcode correspond to a quant
1126 matching_quant_ids = quant_obj.search(cr, uid, [('name', '=', barcode_str)], context=context) # TODO need the location clause
1127 if matching_quant_ids:
1128 todo_on_moves, todo_on_operations = self._deal_with_quants(cr, uid, picking_id, [matching_quant_ids[0]], context=context)
1130 #check if the barcode correspond to a package
1131 matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1132 if matching_package_ids:
1133 included_package_ids = package_obj.search(cr, uid, [('parent_id', 'child_of', matching_package_ids[0])], context=context)
1134 included_quant_ids = quant_obj.search(cr, uid, [('package_id', 'in', included_package_ids)], context=context)
1135 todo_on_moves, todo_on_operations = self._deal_with_quants(cr, uid, picking_id, included_quant_ids, context=context)
1136 #write remaining qty on stock.move, to ease the treatment server side
1137 for todo in todo_on_moves:
1139 self.pool.get('stock.move').write(cr, uid, todo[1], todo[2], context=context)
1141 self.pool.get('stock.move').create(cr, uid, todo[2], context=context)
1142 return {'warnings': error_msg, 'moves_to_update': todo_on_moves, 'operations_to_update': todo_on_operations}
1145 class stock_production_lot(osv.osv):
1146 _name = 'stock.production.lot'
1147 _inherit = ['mail.thread']
1148 _description = 'Lot/Serial'
1150 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number"),
1151 'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"),
1152 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1153 'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants'),
1154 'create_date': fields.datetime('Creation Date'),
1157 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1158 'product_id': lambda x, y, z, c: c.get('product_id', False),
1160 _sql_constraints = [
1161 ('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'),
1165 # ----------------------------------------------------
1167 # ----------------------------------------------------
1169 class stock_move(osv.osv):
1170 _name = "stock.move"
1171 _description = "Stock Move"
1172 _order = 'date_expected desc, id'
1175 def get_price_unit(self, cr, uid, move, context=None):
1176 """ Returns the unit price to store on the quant """
1177 return move.price_unit or move.product_id.standard_price
1179 def name_get(self, cr, uid, ids, context=None):
1181 for line in self.browse(cr, uid, ids, context=context):
1182 name = line.location_id.name + ' > ' + line.location_dest_id.name
1183 if line.product_id.code:
1184 name = line.product_id.code + ': ' + name
1185 if line.picking_id.origin:
1186 name = line.picking_id.origin + '/ ' + name
1187 res.append((line.id, name))
1190 # FP Note: put this on quants, with the auto creation algo
1191 # def _check_tracking(self, cr, uid, ids, context=None):
1192 # """ Checks if serial number is assigned to stock move or not.
1193 # @return: True or False
1195 # for move in self.browse(cr, uid, ids, context=context):
1196 # if not move.lot_id and \
1197 # (move.state == 'done' and \
1199 # (move.product_id.track_production and move.location_id.usage == 'production') or \
1200 # (move.product_id.track_production and move.location_dest_id.usage == 'production') or \
1201 # (move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
1202 # (move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') or \
1203 # (move.product_id.track_incoming and move.location_id.usage == 'inventory') \
1208 def _quantity_normalize(self, cr, uid, ids, name, args, context=None):
1209 uom_obj = self.pool.get('product.uom')
1211 for m in self.browse(cr, uid, ids, context=context):
1212 res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False)
1215 def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
1217 for move in self.browse(cr, uid, ids, context=context):
1218 res[move.id] = move.product_qty
1219 for quant in move.reserved_quant_ids:
1220 res[move.id] -= quant.qty
1223 def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
1224 res = dict.fromkeys(ids, False)
1225 for move in self.browse(cr, uid, ids, context=context):
1226 if move.state == 'done':
1227 res[move.id] = [q.id for q in move.quant_ids]
1229 res[move.id] = [q.id for q in move.reserved_quant_ids]
1232 def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
1233 quant_obj = self.pool.get('stock.quant')
1234 res = dict.fromkeys(ids, False)
1235 for move in self.browse(cr, uid, ids, context=context):
1236 if move.state == 'done':
1237 res[move.id] = move.product_qty
1239 sublocation_ids = self.pool.get('stock.location').search(cr, uid, [('id', 'child_of', [move.location_id.id])], context=context)
1240 quant_ids = quant_obj.search(cr, uid, [('location_id', 'in', sublocation_ids), ('product_id', '=', move.product_id.id), ('reservation_id', '=', False)], context=context)
1242 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1243 availability += quant.qty
1244 res[move.id] = min(move.product_qty, availability)
1247 def _get_move(self, cr, uid, ids, context=None):
1249 for quant in self.browse(cr, uid, ids, context=context):
1250 if quant.reservation_id:
1251 res.add(quant.reservation_id.id)
1255 'name': fields.char('Description', required=True, select=True),
1256 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1257 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1258 'date': fields.datetime('Date', required=True, select=True, help="Move date: scheduled date until move is done, then date of actual move processing", states={'done': [('readonly', True)]}),
1259 'date_expected': fields.datetime('Scheduled Date', states={'done': [('readonly', True)]},required=True, select=True, help="Scheduled date for the processing of this move"),
1260 'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type','<>','service')],states={'done': [('readonly', True)]}),
1261 # TODO: improve store to add dependency on product UoM
1262 'product_qty': fields.function(_quantity_normalize, type='float', store=True, string='Quantity',
1263 digits_compute=dp.get_precision('Product Unit of Measure'),
1264 help='Quantity in the default UoM of the product'),
1265 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
1266 required=True,states={'done': [('readonly', True)]},
1267 help="This is the quantity of products from an inventory "
1268 "point of view. For moves in the state 'done', this is the "
1269 "quantity of products that were actually moved. For other "
1270 "moves, this is the quantity of product that is planned to "
1271 "be moved. Lowering this quantity does not generate a "
1272 "backorder. Changing this quantity on assigned moves affects "
1273 "the product reservation, and should be done with care."
1275 'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True,states={'done': [('readonly', True)]}),
1276 'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}),
1277 'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1279 'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1281 'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True,states={'done': [('readonly', True)]}, help="Sets a location if you produce at a fixed location. This can be a partner location if you subcontract the manufacturing operations."),
1282 'location_dest_id': fields.many2one('stock.location', 'Destination Location', required=True,states={'done': [('readonly', True)]}, select=True, help="Location where the system will stock the finished products."),
1284 # FP Note: should we remove this?
1285 'partner_id': fields.many2one('res.partner', 'Destination Address ', states={'done': [('readonly', True)]}, help="Optional address where goods are to be delivered, specifically used for allotment"),
1288 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True),
1289 'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
1291 'picking_id': fields.many2one('stock.picking', 'Reference', select=True, states={'done': [('readonly', True)]}),
1292 'picking_priority': fields.related('picking_id','priority', type='selection', selection=[('0','Low'),('1','Normal'),('2','High')], string='Picking Priority'),
1293 'note': fields.text('Notes'),
1294 'state': fields.selection([('draft', 'New'),
1295 ('cancel', 'Cancelled'),
1296 ('waiting', 'Waiting Another Move'),
1297 ('confirmed', 'Waiting Availability'),
1298 ('assigned', 'Available'),
1300 ], 'Status', readonly=True, select=True,
1301 help= "* New: When the stock move is created and not yet confirmed.\n"\
1302 "* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"\
1303 "* Waiting Availability: This state is reached when the procurement resolution is not straight forward. It may need the scheduler to run, a component to me manufactured...\n"\
1304 "* Available: When products are reserved, it is set to \'Available\'.\n"\
1305 "* Done: When the shipment is processed, the state is \'Done\'."),
1307 'price_unit': fields.float('Unit Price', help="Technical field used to record the product cost set by the user during a picking confirmation (when average price costing method is used). Value given in company currency and in product uom."), # as it's a technical field, we intentionally don't provide the digits attribute
1309 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1310 'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Order of", select=True),
1311 'origin': fields.char("Source"),
1312 'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', required=True, help="Make to Stock: When needed, the product is taken from the stock or we wait for replenishment. \nMake to Order: When needed, the product is purchased or produced."),
1314 # used for colors in tree views:
1315 'scrapped': fields.related('location_dest_id','scrap_location',type='boolean',relation='stock.location',string='Scrapped', readonly=True),
1317 'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Quants'),
1318 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
1319 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity',
1320 digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]},
1321 store = {'stock.move': (lambda self, cr, uid, ids, c={}: ids , ['product_uom_qty', 'product_uom', 'reserved_quant_ids'], 20),
1322 'stock.quant': (_get_move, ['reservation_id'], 10)}),
1323 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1324 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
1325 'rule_id': fields.many2one('procurement.rule', 'Procurement Rule'),
1326 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
1327 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
1328 'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
1329 'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.quant', string='Lots'),
1330 'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move'),
1331 'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
1332 'availability': fields.function(_get_product_availability, type='float', string='Availability'),
1333 'restrict_lot_id': fields.many2one('stock.production.lot', 'Lot', help="Technical field used to depict a restriction on the lot of quants to consider when marking this move as 'done'"),
1334 'restrict_partner_id': fields.many2one('res.partner', 'Owner ', help="Technical field used to depict a restriction on the ownership of quants to consider when marking this move as 'done'"),
1335 'putaway_ids': fields.one2many('stock.move.putaway', 'move_id', 'Put Away Suggestions'),
1336 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order"),
1337 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Technical field depicting the warehouse to consider for the route selection on the next procurement (if any)."),
1340 def copy(self, cr, uid, id, default=None, context=None):
1343 default = default.copy()
1344 default['move_orig_ids'] = []
1345 default['quant_ids'] = []
1346 default['reserved_quant_ids'] = []
1347 default['returned_move_ids'] = []
1348 default['origin_returned_move_id'] = False
1349 default['state'] = 'draft'
1350 return super(stock_move, self).copy(cr, uid, id, default, context)
1352 def _default_location_destination(self, cr, uid, context=None):
1353 context = context or {}
1354 if context.get('default_picking_type_id', False):
1355 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1356 return pick_type.default_location_dest_id and pick_type.default_location_dest_id.id or False
1359 def _default_location_source(self, cr, uid, context=None):
1360 context = context or {}
1361 if context.get('default_picking_type_id', False):
1362 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1363 return pick_type.default_location_src_id and pick_type.default_location_src_id.id or False
1366 def _default_destination_address(self, cr, uid, context=None):
1367 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1368 return user.company_id.partner_id.id
1371 'location_id': _default_location_source,
1372 'location_dest_id': _default_location_destination,
1373 'partner_id': _default_destination_address,
1377 'product_uom_qty': 1.0,
1379 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1380 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1381 'date_expected': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1382 'procure_method': 'make_to_stock',
1386 def _prepare_procurement_from_move(self, cr, uid, move, context=None):
1387 origin = (move.group_id and (move.group_id.name+":") or "") + (move.rule_id and move.rule_id.name or "/")
1389 'name': move.rule_id and move.rule_id.name or "/",
1391 'company_id': move.company_id and move.company_id.id or False,
1392 'date_planned': move.date,
1393 'product_id': move.product_id.id,
1394 'product_qty': move.product_qty,
1395 'product_uom': move.product_uom.id,
1396 'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_qty,
1397 'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
1398 'location_id': move.location_id.id,
1399 'move_dest_id': move.id,
1400 'group_id': move.group_id and move.group_id.id or False,
1401 'route_ids' : [(4, x.id) for x in move.route_ids],
1402 'warehouse_id': move.warehouse_id and move.warehouse_id.id or False,
1405 def _push_apply(self, cr, uid, moves, context):
1406 push_obj = self.pool.get("stock.location.path")
1408 if not move.move_dest_id:
1409 routes = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids]
1410 routes = routes or [x.id for x in move.route_ids]
1412 domain = [('route_id', 'in', routes), ('location_from_id', '=', move.location_dest_id.id)]
1413 if move.warehouse_id:
1414 domain += [('warehouse_id', '=', move.warehouse_id.id)]
1415 rules = push_obj.search(cr, uid, domain, context=context)
1417 rule = push_obj.browse(cr, uid, rules[0], context=context)
1418 push_obj._apply(cr, uid, rule, move, context=context)
1421 # Create the stock.move.putaway records
1422 def _putaway_apply(self,cr, uid, ids, context=None):
1423 moveputaway_obj = self.pool.get('stock.move.putaway')
1424 for move in self.browse(cr, uid, ids, context=context):
1425 putaway = self.pool.get('stock.location').get_putaway_strategy(cr, uid, move.location_dest_id, move.product_id, context=context)
1427 # Should call different methods here in later versions
1428 # TODO: take care of lots
1429 if putaway.method == 'fixed' and putaway.location_spec_id:
1430 moveputaway_obj.create(cr, SUPERUSER_ID, {'move_id': move.id,
1431 'location_id': putaway.location_spec_id.id,
1432 'quantity': move.product_uom_qty}, context=context)
1435 def _create_procurement(self, cr, uid, move, context=None):
1437 This will create a procurement order
1439 proc_obj = self.pool.get("procurement.order")
1440 return proc_obj.create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
1442 # Check that we do not modify a stock.move which is done
1443 def write(self, cr, uid, ids, vals, context=None):
1444 if isinstance(ids, (int, long)):
1446 frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1447 for move in self.browse(cr, uid, ids, context=context):
1448 if move.state == 'done':
1449 if frozen_fields.intersection(vals):
1450 raise osv.except_osv(_('Operation Forbidden!'),
1451 _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
1452 result = super(stock_move, self).write(cr, uid, ids, vals, context=context)
1455 def onchange_quantity(self, cr, uid, ids, product_id, product_qty,
1456 product_uom, product_uos):
1457 """ On change of product quantity finds UoM and UoS quantities
1458 @param product_id: Product id
1459 @param product_qty: Changed Quantity of product
1460 @param product_uom: Unit of measure of product
1461 @param product_uos: Unit of sale of product
1462 @return: Dictionary of values
1465 'product_uos_qty': 0.00
1469 if (not product_id) or (product_qty <=0.0):
1470 result['product_qty'] = 0.0
1471 return {'value': result}
1473 product_obj = self.pool.get('product.product')
1474 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1476 # Warn if the quantity was decreased
1478 for move in self.read(cr, uid, ids, ['product_qty']):
1479 if product_qty < move['product_qty']:
1481 'title': _('Information'),
1482 'message': _("By changing this quantity here, you accept the "
1483 "new quantity as complete: OpenERP will not "
1484 "automatically generate a back order.") })
1487 if product_uos and product_uom and (product_uom != product_uos):
1488 result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1490 result['product_uos_qty'] = product_qty
1492 return {'value': result, 'warning': warning}
1494 def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1495 product_uos, product_uom):
1496 """ On change of product quantity finds UoM and UoS quantities
1497 @param product_id: Product id
1498 @param product_uos_qty: Changed UoS Quantity of product
1499 @param product_uom: Unit of measure of product
1500 @param product_uos: Unit of sale of product
1501 @return: Dictionary of values
1504 'product_uom_qty': 0.00
1508 if (not product_id) or (product_uos_qty <=0.0):
1509 result['product_uos_qty'] = 0.0
1510 return {'value': result}
1512 product_obj = self.pool.get('product.product')
1513 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1515 # Warn if the quantity was decreased
1516 for move in self.read(cr, uid, ids, ['product_uos_qty']):
1517 if product_uos_qty < move['product_uos_qty']:
1519 'title': _('Warning: No Back Order'),
1520 'message': _("By changing the quantity here, you accept the "
1521 "new quantity as complete: OpenERP will not "
1522 "automatically generate a Back Order.") })
1525 if product_uos and product_uom and (product_uom != product_uos):
1526 result['product_uom_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1528 result['product_uom_qty'] = product_uos_qty
1529 return {'value': result, 'warning': warning}
1531 def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False,
1532 loc_dest_id=False, partner_id=False):
1533 """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
1534 @param prod_id: Changed Product id
1535 @param loc_id: Source location id
1536 @param loc_dest_id: Destination location id
1537 @param partner_id: Address id of partner
1538 @return: Dictionary of values
1542 user = self.pool.get('res.users').browse(cr, uid, uid)
1543 lang = user and user.lang or False
1545 addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id)
1547 lang = addr_rec and addr_rec.lang or False
1548 ctx = {'lang': lang}
1550 product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1551 uos_id = product.uos_id and product.uos_id.id or False
1553 'product_uom': product.uom_id.id,
1554 'product_uos': uos_id,
1555 'product_uom_qty': 1.00,
1556 'product_uos_qty' : self.pool.get('stock.move').onchange_quantity(cr, uid, ids, prod_id, 1.00, product.uom_id.id, uos_id)['value']['product_uos_qty'],
1559 result['name'] = product.partner_ref
1561 result['location_id'] = loc_id
1563 result['location_dest_id'] = loc_dest_id
1564 return {'value': result}
1566 def _picking_assign(self, cr, uid, move, context=None):
1567 if move.picking_id or not move.picking_type_id:
1569 context = context or {}
1570 pick_obj = self.pool.get("stock.picking")
1572 group = move.group_id and move.group_id.id or False
1573 picks = pick_obj.search(cr, uid, [
1574 ('group_id', '=', group),
1575 ('location_id', '=', move.location_id.id),
1576 ('location_dest_id', '=', move.location_dest_id.id),
1577 ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context)
1582 'origin': move.origin,
1583 'company_id': move.company_id and move.company_id.id or False,
1584 'move_type': move.group_id and move.group_id.move_type or 'one',
1585 'partner_id': move.group_id and move.group_id.partner_id and move.group_id.partner_id.id or False,
1586 'date_done': move.date_expected,
1587 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
1589 pick = pick_obj.create(cr, uid, values, context=context)
1590 move.write({'picking_id': pick})
1593 def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
1594 """ On change of Scheduled Date gives a Move date.
1595 @param date_expected: Scheduled Date
1596 @param date: Move Date
1599 if not date_expected:
1600 date_expected = time.strftime('%Y-%m-%d %H:%M:%S')
1601 return {'value':{'date': date_expected}}
1603 def action_confirm(self, cr, uid, ids, context=None):
1604 """ Confirms stock move or put it in waiting if it's linked to another move.
1605 @return: List of ids.
1611 for move in self.browse(cr, uid, ids, context=context):
1613 for m in move.move_orig_ids:
1614 if m.state not in ('done', 'cancel'):
1616 states[state].append(move.id)
1617 self._picking_assign(cr, uid, move, context=context)
1619 for state, write_ids in states.items():
1621 self.write(cr, uid, write_ids, {'state': state})
1622 if state == 'confirmed':
1623 for move in self.browse(cr, uid, write_ids, context=context):
1624 if move.procure_method == 'make_to_order':
1625 self._create_procurement(cr, uid, move, context=context)
1626 moves = self.browse(cr, uid, ids, context=context)
1627 self._push_apply(cr, uid, moves, context=context)
1630 def force_assign(self, cr, uid, ids, context=None):
1631 """ Changes the state to assigned.
1634 done = self.action_assign(cr, uid, ids, context=context)
1635 self.write(cr, uid, list(set(ids) - set(done)), {'state': 'assigned'})
1639 def cancel_assign(self, cr, uid, ids, context=None):
1640 """ Changes the state to confirmed.
1643 return self.write(cr, uid, ids, {'state': 'confirmed'})
1645 def action_assign(self, cr, uid, ids, context=None):
1646 """ Checks the product type and accordingly writes the state.
1647 @return: No. of moves done
1649 context = context or {}
1650 quant_obj = self.pool.get("stock.quant")
1652 for move in self.browse(cr, uid, ids, context=context):
1653 if move.state not in ('confirmed', 'waiting'):
1655 if move.product_id.type == 'consu':
1656 done.append(move.id)
1659 qty = move.product_qty
1661 for m2 in move.move_orig_ids:
1662 for q in m2.quant_ids:
1663 dp.append(str(q.id))
1665 domain = ['|', ('reservation_id', '=', False), ('reservation_id', '=', move.id), ('qty', '>', 0)]
1666 prefered_domain = dp and [('id', 'not in', dp)] or []
1667 fallback_domain = dp and [('id', 'in', dp)] or []
1668 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=domain, prefered_domain=prefered_domain, fallback_domain=fallback_domain, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
1669 #Will only reserve physical quants, no negative
1670 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
1671 # the total quantity is provided by existing quants
1672 if all(map(lambda x:x[0], quants)):
1673 done.append(move.id)
1674 self.write(cr, uid, done, {'state': 'assigned'})
1675 self._putaway_apply(cr, uid, ids, context=context)
1680 # Cancel move => cancel others move and pickings
1682 def action_cancel(self, cr, uid, ids, context=None):
1683 """ Cancels the moves and if all moves are cancelled it cancels the picking.
1686 procurement_obj = self.pool.get('procurement.order')
1687 context = context or {}
1688 for move in self.browse(cr, uid, ids, context=context):
1689 if move.state == 'done':
1690 raise osv.except_osv(_('Operation Forbidden!'),
1691 _('You cannot cancel a stock move that has been set to \'Done\'.'))
1692 if move.reserved_quant_ids:
1693 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
1694 if context.get('cancel_procurement'):
1696 procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
1697 procurement_obj.cancel(cr, uid, procurement_ids, context=context)
1698 elif move.move_dest_id:
1699 #cancel chained moves
1701 self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
1702 elif move.move_dest_id.state == 'waiting':
1703 self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'})
1704 return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1706 #def _get_quants_from_pack(self, cr, uid, ids, context=None):
1708 # Suppose for the moment we don't have any packaging
1711 # for move in self.browse(cr, uid, ids, context=context):
1712 # #Split according to pack wizard if necessary
1713 # res[move.id] = [x.id for x in move.reserved_quant_ids]
1716 def action_done(self, cr, uid, ids, negatives = False, context=None):
1717 """ Makes the move done and if all moves are done, it will finish the picking.
1718 If quants are not assigned yet, it should assign them
1719 Putaway strategies should be applied
1722 context = context or {}
1723 quant_obj = self.pool.get("stock.quant")
1724 ops_obj = self.pool.get("stock.pack.operation")
1725 pack_obj = self.pool.get("stock.quant.package")
1726 todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
1728 self.action_confirm(cr, uid, todo, context=context)
1731 procurement_ids = []
1732 for move in self.browse(cr, uid, ids, context=context):
1733 # if negatives and negatives[move.id]:
1734 # for ops in negatives[move.id].keys():
1735 # quants_to_move = [(None, negatives[move.id, x) for x in negatives]
1736 # quant_obj.quants_move(cr, uid, quants_to_move, move, context=context)
1739 pickings.add(move.picking_id.id)
1740 qty = move.product_qty
1742 # for qty, location_id in move_id.prefered_location_ids:
1743 # quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, context=context)
1744 # quant_obj.quants_move(cr, uid, quants, move, location_dest_id, context=context)
1745 # should replace the above 2 lines
1746 dom = [('qty', '>', 0)]
1747 prefered_domain = [('reservation_id', '=', move.id)]
1748 fallback_domain = [('reservation_id', '=', False)]
1749 if move.picking_id and move.picking_id.pack_operation_ids:
1750 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty - move.remaining_qty, domain=dom, prefered_domain=prefered_domain, fallback_domain=fallback_domain, context=context)
1751 quant_obj.quants_move(cr, uid, quants, move, context=context)
1752 if negatives and move.id in negatives:
1753 for negative_op in negatives[move.id].keys():
1754 ops = ops_obj.browse(cr, uid, negative_op, context=context)
1755 negatives[move.id][negative_op] = quant_obj.quants_move(cr, uid, [(None, negatives[move.id][negative_op])], move,
1756 lot_id = ops.lot_id and ops.lot_id.id or False,
1757 owner_id = ops.owner_id and ops.owner_id.id or False,
1758 package_id = ops.package_id and ops.package_id.id or False, context=context)
1760 reserved_ops = list(set([x.reservation_op_id.id for x in move.reserved_quant_ids]))
1761 for ops in ops_obj.browse(cr, uid, reserved_ops, context=context):
1763 quant_obj.write(cr, uid, [x.id for x in ops.reserved_quant_ids], {'package_id': ops.result_package_id and ops.result_package_id.id or False}, context=context)
1765 pack_obj.write(cr, uid, [ops.package_id.id], {'parent_id': ops.result_package_id and ops.result_package_id.id or False}, context=context)
1767 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_domain=prefered_domain, fallback_domain=fallback_domain, context=context)
1768 #Will move all quants_get and as such create negative quants
1769 quant_obj.quants_move(cr, uid, quants, move, context=context)
1770 quant_obj.quants_unreserve(cr, uid, move, context=context)
1772 #Check moves that were pushed
1773 if move.move_dest_id.state in ('waiting', 'confirmed'):
1774 other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
1775 ('move_dest_id', '=', move.move_dest_id.id)], context=context)
1776 #If no other moves for the move that got pushed:
1777 if not other_upstream_move_ids and move.move_dest_id.state in ('waiting', 'confirmed'):
1778 self.action_assign(cr, uid, [move.move_dest_id.id], context=context)
1779 if move.procurement_id:
1780 procurement_ids.append(move.procurement_id.id)
1781 self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1782 self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
1785 def unlink(self, cr, uid, ids, context=None):
1786 context = context or {}
1787 for move in self.browse(cr, uid, ids, context=context):
1788 if move.state not in ('draft', 'cancel'):
1789 raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
1790 return super(stock_move, self).unlink(cr, uid, ids, context=context)
1792 def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
1793 """ Move the scrap/damaged product into scrap location
1794 @param cr: the database cursor
1795 @param uid: the user id
1796 @param ids: ids of stock move object to be scrapped
1797 @param quantity : specify scrap qty
1798 @param location_id : specify scrap location
1799 @param context: context arguments
1800 @return: Scraped lines
1802 #quantity should in MOVE UOM
1804 raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
1806 for move in self.browse(cr, uid, ids, context=context):
1807 source_location = move.location_id
1808 if move.state == 'done':
1809 source_location = move.location_dest_id
1810 #Previously used to prevent scraping from virtual location but not necessary anymore
1811 #if source_location.usage != 'internal':
1812 #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
1813 #raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
1814 move_qty = move.product_qty
1815 uos_qty = quantity / move_qty * move.product_uos_qty
1817 'location_id': source_location.id,
1818 'product_uom_qty': quantity,
1819 'product_uos_qty': uos_qty,
1820 'state': move.state,
1822 'location_dest_id': location_id,
1823 #TODO lot_id is now on quant and not on move, need to do something for this
1824 #'lot_id': move.lot_id.id,
1826 new_move = self.copy(cr, uid, move.id, default_val)
1829 product_obj = self.pool.get('product.product')
1830 for product in product_obj.browse(cr, uid, [move.product_id.id], context=context):
1832 uom = product.uom_id.name if product.uom_id else ''
1833 message = _("%s %s %s has been <b>moved to</b> scrap.") % (quantity, uom, product.name)
1834 move.picking_id.message_post(body=message)
1836 self.action_done(cr, uid, res, context=context)
1839 def action_consume(self, cr, uid, ids, quantity, location_id=False, context=None):
1840 """ Consumed product with specific quatity from specific source location
1841 @param cr: the database cursor
1842 @param uid: the user id
1843 @param ids: ids of stock move object to be consumed
1844 @param quantity : specify consume quantity
1845 @param location_id : specify source location
1846 @param context: context arguments
1847 @return: Consumed lines
1849 #quantity should in MOVE UOM
1853 raise osv.except_osv(_('Warning!'), _('Please provide proper quantity.'))
1855 for move in self.browse(cr, uid, ids, context=context):
1856 move_qty = move.product_qty
1858 raise osv.except_osv(_('Error!'), _('Cannot consume a move with negative or zero quantity.'))
1859 quantity_rest = move.product_qty
1860 quantity_rest -= quantity
1861 uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
1862 if quantity_rest <= 0:
1865 quantity = move.product_qty
1867 uos_qty = quantity / move_qty * move.product_uos_qty
1868 if quantity_rest > 0:
1870 'product_uom_qty': quantity,
1871 'product_uos_qty': uos_qty,
1872 'state': move.state,
1873 'location_id': location_id or move.location_id.id,
1875 current_move = self.copy(cr, uid, move.id, default_val)
1876 res += [current_move]
1878 update_val['product_uom_qty'] = quantity_rest
1879 update_val['product_uos_qty'] = uos_qty_rest
1880 self.write(cr, uid, [move.id], update_val)
1883 quantity_rest = quantity
1884 uos_qty_rest = uos_qty
1887 'product_uom_qty' : quantity_rest,
1888 'product_uos_qty' : uos_qty_rest,
1889 'location_id': location_id or move.location_id.id,
1891 self.write(cr, uid, [move.id], update_val)
1893 self.action_done(cr, uid, res, context=context)
1898 def split(self, cr, uid, move, qty, context=None):
1900 Splits qty from move move into a new move
1902 if move.product_qty==qty:
1904 if (move.product_qty < qty) or (qty==0):
1907 uom_obj = self.pool.get('product.uom')
1908 context = context or {}
1910 uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id)
1911 uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
1913 if move.state in ('done', 'cancel'):
1914 raise osv.except_osv(_('Error'), _('You cannot split a move done'))
1917 'product_uom_qty': uom_qty,
1918 'product_uos_qty': uos_qty,
1919 'state': move.state,
1920 'move_dest_id': False,
1921 'reserved_quant_ids': []
1923 new_move = self.copy(cr, uid, move.id, defaults)
1925 self.write(cr, uid, [move.id], {
1926 'product_uom_qty': move.product_uom_qty - uom_qty,
1927 'product_uos_qty': move.product_uos_qty - uos_qty,
1928 # 'reserved_quant_ids': [(6,0,[])] SHOULD NOT CHANGE as it has been reserved already
1931 if move.move_dest_id and move.propagate:
1932 new_move_prop = self.split(cr, uid, move.move_dest_id, qty, context=context)
1933 self.write(cr, uid, [new_move], {'move_dest_id': new_move_prop}, context=context)
1935 self.action_confirm(cr, uid, [new_move], context=context)
1938 class stock_inventory(osv.osv):
1939 _name = "stock.inventory"
1940 _description = "Inventory"
1942 def _get_move_ids_exist(self, cr, uid, ids, field_name, arg, context=None):
1944 for inv in self.browse(cr, uid, ids, context=context):
1951 'name': fields.char('Inventory Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
1952 'date': fields.datetime('Inventory Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Create Date."),
1953 'date_done': fields.datetime('Date done', help="Inventory Validation Date."),
1954 'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines."),
1955 'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves."),
1956 'state': fields.selection([('draft', 'Draft'), ('cancel', 'Cancelled'), ('confirm', 'In Progress'), ('done', 'Validated')], 'Status', readonly=True, select=True),
1957 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
1958 'location_id': fields.many2one('stock.location', 'Location', required=True),
1959 'product_id': fields.many2one('product.product', 'Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
1960 'package_id': fields.many2one('stock.quant.package', 'Pack', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Pack to focus your inventory on a particular Pack."),
1961 'partner_id': fields.many2one('res.partner', 'Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
1962 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number."),
1963 'move_ids_exist': fields.function(_get_move_ids_exist, type='boolean', string=' Stock Move Exists?', help='technical field for attrs in view'),
1964 'filter': fields.selection([('product', 'Product'), ('owner', 'Owner'), ('product_owner','Product & Owner'), ('lot','Lot/Serial Number'), ('pack','Pack'), ('none', 'None')], 'Selection Filter'),
1967 def _default_stock_location(self, cr, uid, context=None):
1969 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
1970 return warehouse.lot_stock_id.id
1975 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1977 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
1978 'location_id': _default_stock_location,
1981 def set_checked_qty(self, cr, uid, ids, context=None):
1982 inventory = self.browse(cr, uid, ids[0], context=context)
1983 line_ids = [line.id for line in inventory.line_ids]
1984 self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
1987 def copy(self, cr, uid, id, default=None, context=None):
1990 default = default.copy()
1991 default.update({'move_ids': [], 'date_done': False})
1992 return super(stock_inventory, self).copy(cr, uid, id, default, context=context)
1994 def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
1995 """ Creates a stock move from an inventory line
1996 @param inventory_line:
2000 return self.pool.get('stock.move').create(cr, uid, move_vals)
2002 def action_done(self, cr, uid, ids, context=None):
2003 """ Finish the inventory
2008 move_obj = self.pool.get('stock.move')
2009 for inv in self.browse(cr, uid, ids, context=context):
2010 if not inv.move_ids:
2011 self.action_check(cr, uid, [inv.id], context=context)
2013 #the action_done on stock_move has to be done in 2 steps:
2014 #first, we start moving the products from stock to inventory loss
2015 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage == 'internal'], context=context)
2016 #then, we move from inventory loss. This 2 steps process is needed because some moved quant may need to be put again in stock
2017 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage != 'internal'], context=context)
2018 self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
2021 def _create_stock_move(self, cr, uid, inventory, todo_line, context=None):
2022 stock_move_obj = self.pool.get('stock.move')
2023 product_obj = self.pool.get('product.product')
2024 inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id
2026 'name': _('INV:') + (inventory.name or ''),
2027 'product_id': todo_line['product_id'],
2028 'product_uom': todo_line['product_uom_id'],
2029 'date': inventory.date,
2030 'company_id': inventory.company_id.id,
2031 'inventory_id': inventory.id,
2032 'state': 'assigned',
2033 'restrict_lot_id': todo_line.get('prod_lot_id'),
2034 'restrict_partner_id': todo_line.get('partner_id'),
2037 if todo_line['product_qty'] < 0:
2038 #found more than expected
2039 vals['location_id'] = inventory_location_id
2040 vals['location_dest_id'] = todo_line['location_id']
2041 vals['product_uom_qty'] = -todo_line['product_qty']
2043 #found less than expected
2044 vals['location_id'] = todo_line['location_id']
2045 vals['location_dest_id'] = inventory_location_id
2046 vals['product_uom_qty'] = todo_line['product_qty']
2047 return stock_move_obj.create(cr, uid, vals, context=context)
2049 def action_check(self, cr, uid, ids, context=None):
2050 """ Checks the inventory and computes the stock move to do
2053 inventory_line_obj = self.pool.get('stock.inventory.line')
2054 stock_move_obj = self.pool.get('stock.move')
2055 for inventory in self.browse(cr, uid, ids, context=context):
2056 #first remove the existing stock moves linked to this inventory
2057 move_ids = [move.id for move in inventory.move_ids]
2058 stock_move_obj.unlink(cr, uid, move_ids, context=context)
2059 #compute what should be in the inventory lines
2060 theorical_lines = self._get_inventory_lines(cr, uid, inventory, context=context)
2061 for line in inventory.line_ids:
2062 #compare the inventory lines to the theorical ones and store the diff in theorical_lines
2063 inventory_line_obj._resolve_inventory_line(cr, uid, line, theorical_lines, context=context)
2064 #each theorical_lines where product_qty is not 0 is a difference for which we need to create a stock move
2065 for todo_line in theorical_lines:
2066 if todo_line['product_qty'] != 0:
2067 self._create_stock_move(cr, uid, inventory, todo_line, context=context)
2069 def action_cancel_draft(self, cr, uid, ids, context=None):
2070 """ Cancels the stock move and change inventory state to draft.
2073 for inv in self.browse(cr, uid, ids, context=context):
2074 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2075 self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
2078 def action_cancel_inventory(self, cr, uid, ids, context=None):
2080 self.action_cancel_draft(cr, uid, ids, context=context)
2082 def prepare_inventory(self, cr, uid, ids, context=None):
2083 inventory_line_obj = self.pool.get('stock.inventory.line')
2084 for inventory in self.browse(cr, uid, ids, context=context):
2085 #clean the existing inventory lines before redoing an inventory proposal
2086 line_ids = [line.id for line in inventory.line_ids]
2087 inventory_line_obj.unlink(cr, uid, line_ids, context=context)
2088 #compute the inventory lines and create them
2089 vals = self._get_inventory_lines(cr, uid, inventory, context=context)
2090 for product_line in vals:
2091 inventory_line_obj.create(cr, uid, product_line, context=context)
2092 return self.write(cr, uid, ids, {'state': 'confirm'})
2094 def _get_inventory_lines(self, cr, uid, inventory, context=None):
2095 location_obj = self.pool.get('stock.location')
2096 product_obj = self.pool.get('product.product')
2097 location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
2098 domain = ' location_id in %s'
2099 args = (tuple(location_ids),)
2100 if inventory.partner_id:
2101 domain += ' and owner_id = %s'
2102 args += (inventory.partner_id.id,)
2103 if inventory.lot_id:
2104 domain += ' and lot_id = %s'
2105 args += (inventory.lot_id.id,)
2106 if inventory.product_id:
2107 domain += 'and product_id = %s'
2108 args += (inventory.product_id.id,)
2109 if inventory.package_id:
2110 domain += ' and package_id = %s'
2111 args += (inventory.package_id.id,)
2113 SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
2114 FROM stock_quant WHERE''' + domain + '''
2115 GROUP BY product_id, location_id, lot_id, package_id, partner_id
2118 for product_line in cr.dictfetchall():
2119 #replace the None the dictionary by False, because falsy values are tested later on
2120 for key, value in product_line.items():
2122 product_line[key] = False
2123 product_line['inventory_id'] = inventory.id
2124 product_line['th_qty'] = product_line['product_qty']
2125 if product_line['product_id']:
2126 product_line['product_uom_id'] = product_obj.browse(cr, uid, product_line['product_id'], context=context).uom_id.id
2127 vals.append(product_line)
2130 class stock_inventory_line(osv.osv):
2131 _name = "stock.inventory.line"
2132 _description = "Inventory Line"
2133 _rec_name = "inventory_id"
2135 'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2136 'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
2137 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2138 'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
2139 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
2140 'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
2141 'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
2142 'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
2143 'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
2144 'th_qty': fields.float('Theoretical Quantity', readonly=True),
2145 'partner_id': fields.many2one('res.partner', 'Owner'),
2152 def _resolve_inventory_line(self, cr, uid, inventory_line, theorical_lines, context=None):
2153 #TODO : package_id management !
2155 uom_obj = self.pool.get('product.uom')
2156 for th_line in theorical_lines:
2157 #We try to match the inventory line with a theorical line with same product, lot, location and owner
2158 if th_line['location_id'] == inventory_line.location_id.id and th_line['product_id'] == inventory_line.product_id.id and th_line['prod_lot_id'] == inventory_line.prod_lot_id.id and th_line['partner_id'] == inventory_line.partner_id.id:
2159 uom_reference = inventory_line.product_id.uom_id
2160 real_qty = uom_obj._compute_qty_obj(cr, uid, inventory_line.product_uom_id, inventory_line.product_qty, uom_reference)
2161 th_line['product_qty'] -= real_qty
2164 #if it was still not found, we add it to the theorical lines so that it will create a stock move for it
2167 'inventory_id': inventory_line.inventory_id.id,
2168 'location_id': inventory_line.location_id.id,
2169 'product_id': inventory_line.product_id.id,
2170 'product_uom_id': inventory_line.product_id.uom_id.id,
2171 'product_qty': -inventory_line.product_qty,
2172 'prod_lot_id': inventory_line.prod_lot_id.id,
2173 'partner_id': inventory_line.partner_id.id,
2175 theorical_lines.append(vals)
2177 def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False, owner_id=False, lot_id=False, package_id=False, context=None):
2178 """ Changes UoM and name if product_id changes.
2179 @param location_id: Location id
2180 @param product: Changed product_id
2181 @param uom: UoM product
2182 @return: Dictionary of changed values
2184 context = context or {}
2186 return {'value': {'product_qty': 0.0, 'product_uom_id': False}}
2187 uom_obj = self.pool.get('product.uom')
2188 ctx = context.copy()
2189 ctx['location'] = location_id
2190 ctx['lot_id'] = lot_id
2191 ctx['owner_id'] = owner_id
2192 ctx['package_id'] = package_id
2193 obj_product = self.pool.get('product.product').browse(cr, uid, product, context=ctx)
2194 th_qty = obj_product.qty_available
2195 if uom and uom != obj_product.uom_id.id:
2196 uom_record = uom_obj.browse(cr, uid, uom, context=context)
2197 th_qty = uom_obj._compute_qty_obj(cr, uid, obj_product.uom_id, th_qty, uom_record)
2198 return {'value': {'th_qty': th_qty, 'product_uom_id': uom or obj_product.uom_id.id}}
2201 #----------------------------------------------------------
2203 #----------------------------------------------------------
2204 class stock_warehouse(osv.osv):
2205 _name = "stock.warehouse"
2206 _description = "Warehouse"
2209 'name': fields.char('Name', size=128, required=True, select=True),
2210 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
2211 'partner_id': fields.many2one('res.partner', 'Address'),
2212 'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]),
2213 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage', '=', 'internal')]),
2214 'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"),
2215 'route_ids': fields.many2many('stock.location.route', 'stock_route_warehouse', 'warehouse_id', 'route_id', 'Routes', domain="[('warehouse_selectable', '=', True)]", help='Defaults routes through the warehouse'),
2216 'reception_steps': fields.selection([
2217 ('one_step', 'Receive goods directly in stock (1 step)'),
2218 ('two_steps', 'Unload in input location then go to stock (2 steps)'),
2219 ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments', required=True),
2220 'delivery_steps': fields.selection([
2221 ('ship_only', 'Ship directly from stock (Ship only)'),
2222 ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
2223 ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], 'Outgoing Shippings', required=True),
2224 'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'),
2225 'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'),
2226 'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'),
2227 'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'),
2228 'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'),
2229 'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'),
2230 'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'),
2231 'out_type_id': fields.many2one('stock.picking.type', 'Out Type'),
2232 'in_type_id': fields.many2one('stock.picking.type', 'In Type'),
2233 'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'),
2234 'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'),
2235 'reception_route_id': fields.many2one('stock.location.route', 'Reception Route'),
2236 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'),
2237 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'),
2238 'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'),
2239 'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes'),
2240 'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse'),
2243 def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None):
2244 resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))])
2245 if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id
2246 resupply_wh_ids.add(default_resupply_wh_id)
2247 resupply_wh_ids = list(resupply_wh_ids)
2248 return {'value': {'resupply_wh_ids': resupply_wh_ids}}
2250 def _get_inter_wh_location(self, cr, uid, warehouse, context=None):
2251 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2252 data_obj = self.pool.get('ir.model.data')
2254 inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1]
2256 inter_wh_loc = False
2259 def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None):
2260 return self.pool.get('product.product').search(cr, uid, [], context=context)
2262 def _assign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2263 product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2264 self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2266 def _unassign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2267 product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2268 self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(3, inter_wh_route_id)]}, context=context)
2270 def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2272 'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2273 'warehouse_selectable': False,
2274 'product_selectable': True,
2275 'product_categ_selectable': True,
2276 'supplied_wh_id': warehouse.id,
2277 'supplier_wh_id': wh.id,
2280 def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2281 location_obj = self.pool.get('stock.location')
2282 route_obj = self.pool.get('stock.location.route')
2283 pull_obj = self.pool.get('procurement.rule')
2284 #create route selectable on the product to resupply the warehouse from another one
2285 inter_wh_location_id = self._get_inter_wh_location(cr, uid, warehouse, context=context)
2286 if inter_wh_location_id:
2287 input_loc = warehouse.wh_input_stock_loc_id
2288 if warehouse.reception_steps == 'one_step':
2289 input_loc = warehouse.lot_stock_id
2290 inter_wh_location = location_obj.browse(cr, uid, inter_wh_location_id, context=context)
2291 for wh in supplier_warehouses:
2292 output_loc = wh.wh_output_stock_loc_id
2293 if wh.delivery_steps == 'ship_only':
2294 output_loc = wh.lot_stock_id
2295 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2296 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2297 values = [(output_loc, inter_wh_location, wh.out_type_id.id, wh), (inter_wh_location, input_loc, warehouse.in_type_id.id, warehouse)]
2298 pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context)
2299 for pull_rule in pull_rules_list:
2300 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2301 #if the warehouse is also set as default resupply method, assign this route automatically to all product
2302 if default_resupply_wh and default_resupply_wh.id == wh.id:
2303 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2304 #finally, save the route on the warehouse
2305 self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2307 def _default_stock_id(self, cr, uid, context=None):
2308 #lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock')
2310 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2311 return warehouse.lot_stock_id.id
2316 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2317 'lot_stock_id': _default_stock_id,
2318 'reception_steps': 'one_step',
2319 'delivery_steps': 'ship_only',
2321 _sql_constraints = [
2322 ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2323 ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2324 ('default_resupply_wh_diff', 'check (id != default_resupply_wh_id)', 'The default resupply warehouse should be different that the warehouse itself!'),
2327 def _get_partner_locations(self, cr, uid, ids, context=None):
2328 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2329 data_obj = self.pool.get('ir.model.data')
2330 location_obj = self.pool.get('stock.location')
2332 customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2333 supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2335 customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2336 customer_loc = customer_loc and customer_loc[0] or False
2337 supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2338 supplier_loc = supplier_loc and supplier_loc[0] or False
2339 if not (customer_loc and supplier_loc):
2340 raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2341 return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2343 def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2344 location_obj = self.pool.get('stock.location')
2346 new_reception_step = new_reception_step or warehouse.reception_steps
2347 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2348 if warehouse.reception_steps != new_reception_step:
2349 location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2350 if new_reception_step != 'one_step':
2351 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2352 if new_reception_step == 'three_steps':
2353 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2355 if warehouse.delivery_steps != new_delivery_step:
2356 location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
2357 if new_delivery_step != 'ship_only':
2358 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
2359 if new_delivery_step == 'pick_pack_ship':
2360 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
2363 def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
2365 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2366 'product_categ_selectable': True,
2367 'product_selectable': False,
2371 def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None):
2372 pull_rules_list = []
2373 for from_loc, dest_loc, pick_type_id, warehouse in values:
2374 pull_rules_list.append({
2375 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2376 'location_src_id': from_loc.id,
2377 'location_id': dest_loc.id,
2378 'route_id': new_route_id,
2380 'picking_type_id': pick_type_id,
2381 'procure_method': 'make_to_order',
2382 'warehouse_id': supplied_warehouse.id,
2384 return pull_rules_list
2386 def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
2388 push_rules_list = []
2389 pull_rules_list = []
2390 for from_loc, dest_loc, pick_type_id in values:
2391 push_rules_list.append({
2392 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2393 'location_from_id': from_loc.id,
2394 'location_dest_id': dest_loc.id,
2395 'route_id': new_route_id,
2397 'picking_type_id': pick_type_id,
2399 'warehouse_id': warehouse.id,
2401 pull_rules_list.append({
2402 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2403 'location_src_id': from_loc.id,
2404 'location_id': dest_loc.id,
2405 'route_id': new_route_id,
2407 'picking_type_id': pick_type_id,
2408 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
2410 'warehouse_id': warehouse.id,
2413 return push_rules_list, pull_rules_list
2415 def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
2416 route_obj = self.pool.get('stock.location.route')
2417 data_obj = self.pool.get('ir.model.data')
2419 mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
2421 mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('MTO'))], context=context)
2422 mto_route_id = mto_route_id and mto_route_id[0] or False
2423 if not mto_route_id:
2424 raise osv.except_osv(_('Error!'), _('Can\'t find any generic MTO route.'))
2426 from_loc, dest_loc, pick_type_id = values[0]
2428 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
2429 'location_src_id': from_loc.id,
2430 'location_id': dest_loc.id,
2431 'route_id': mto_route_id,
2433 'picking_type_id': pick_type_id,
2434 'procure_method': 'make_to_order',
2436 'warehouse_id': warehouse.id,
2439 def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
2441 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2442 'warehouse_selectable': False,
2443 'product_selectable': True,
2444 'product_categ_selectable': True,
2445 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
2449 def create_routes(self, cr, uid, ids, warehouse, context=None):
2451 route_obj = self.pool.get('stock.location.route')
2452 pull_obj = self.pool.get('procurement.rule')
2453 push_obj = self.pool.get('stock.location.path')
2454 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2455 #create reception route and rules
2456 route_name, values = routes_dict[warehouse.reception_steps]
2457 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2458 reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
2459 wh_route_ids.append((4, reception_route_id))
2460 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
2461 #create the push/pull rules
2462 for push_rule in push_rules_list:
2463 push_obj.create(cr, uid, vals=push_rule, context=context)
2464 for pull_rule in pull_rules_list:
2465 #all pull rules in reception route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location
2466 pull_rule['procure_method'] = 'make_to_order'
2467 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2469 #create MTS route and pull rules for delivery a specific route MTO to be set on the product
2470 route_name, values = routes_dict[warehouse.delivery_steps]
2471 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2472 #create the route and its pull rules
2473 delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
2474 wh_route_ids.append((4, delivery_route_id))
2475 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
2476 for pull_rule in pull_rules_list:
2477 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2478 #create MTO pull rule and link it to the generic MTO route
2479 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2480 mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
2482 #create a route for cross dock operations, that can be set on products and product categories
2483 route_name, values = routes_dict['crossdock']
2484 crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
2485 crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
2486 wh_route_ids.append((4, crossdock_route_id))
2487 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', values, crossdock_route_id, context=context)
2488 for pull_rule in pull_rules_list:
2489 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2491 #create route selectable on the product to resupply the warehouse from another one
2492 self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
2494 #return routes and mto pull rule to store on the warehouse
2496 'route_ids': wh_route_ids,
2497 'mto_pull_id': mto_pull_id,
2498 'reception_route_id': reception_route_id,
2499 'delivery_route_id': delivery_route_id,
2500 'crossdock_route_id': crossdock_route_id,
2503 def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2504 picking_type_obj = self.pool.get('stock.picking.type')
2505 pull_obj = self.pool.get('procurement.rule')
2506 push_obj = self.pool.get('stock.location.path')
2507 route_obj = self.pool.get('stock.location.route')
2508 new_reception_step = new_reception_step or warehouse.reception_steps
2509 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2511 #change the default source and destination location and (de)activate picking types
2512 input_loc = warehouse.wh_input_stock_loc_id
2513 if new_reception_step == 'one_step':
2514 input_loc = warehouse.lot_stock_id
2515 output_loc = warehouse.wh_output_stock_loc_id
2516 if new_delivery_step == 'ship_only':
2517 output_loc = warehouse.lot_stock_id
2518 picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
2519 picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
2520 picking_type_obj.write(cr, uid, warehouse.int_type_id.id, {'active': new_reception_step != 'one_step'}, context=context)
2521 picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
2522 picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
2524 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2525 #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
2526 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
2527 route_name, values = routes_dict[new_delivery_step]
2528 route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2529 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
2530 #create the pull rules
2531 for pull_rule in pull_rules_list:
2532 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2534 #update reception route and rules: unlink the existing rules of the warehouse reception route and recreate it
2535 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
2536 push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
2537 route_name, values = routes_dict[new_reception_step]
2538 route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2539 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
2540 #create the push/pull rules
2541 for push_rule in push_rules_list:
2542 push_obj.create(cr, uid, vals=push_rule, context=context)
2543 for pull_rule in pull_rules_list:
2544 #all pull rules in reception route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location
2545 pull_rule['procure_method'] = 'make_to_order'
2546 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2548 route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
2551 dummy, values = routes_dict[new_delivery_step]
2552 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2553 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
2556 def create(self, cr, uid, vals, context=None):
2561 data_obj = self.pool.get('ir.model.data')
2562 seq_obj = self.pool.get('ir.sequence')
2563 picking_type_obj = self.pool.get('stock.picking.type')
2564 location_obj = self.pool.get('stock.location')
2566 #create view location for warehouse
2567 wh_loc_id = location_obj.create(cr, uid, {
2568 'name': _(vals.get('name')),
2570 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1]
2572 vals['view_location_id'] = wh_loc_id
2573 #create all location
2574 reception_steps = vals.get('reception_steps', False)
2575 delivery_steps = vals.get('delivery_steps', False)
2576 context_with_inactive = context.copy()
2577 context_with_inactive['active_test'] = False
2579 {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
2580 {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
2581 {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
2582 {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
2583 {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
2585 for values in sub_locations:
2586 location_id = location_obj.create(cr, uid, {
2587 'name': values['name'],
2588 'usage': 'internal',
2589 'location_id': wh_loc_id,
2590 'active': values['active'],
2591 }, context=context_with_inactive)
2592 vals[values['field']] = location_id
2594 #create new sequences
2595 in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence in'), 'prefix': vals.get('code', '') + '\IN\\', 'padding': 5}, context=context)
2596 out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence out'), 'prefix': vals.get('code', '') + '\OUT\\', 'padding': 5}, context=context)
2597 pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence packing'), 'prefix': vals.get('code', '') + '\PACK\\', 'padding': 5}, context=context)
2598 pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence picking'), 'prefix': vals.get('code', '') + '\PICK\\', 'padding': 5}, context=context)
2599 int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence internal'), 'prefix': vals.get('code', '') + '\INT\\', 'padding': 5}, context=context)
2602 new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
2604 warehouse = self.browse(cr, uid, new_id, context=context)
2605 wh_stock_loc = warehouse.lot_stock_id
2606 wh_input_stock_loc = warehouse.wh_input_stock_loc_id
2607 wh_output_stock_loc = warehouse.wh_output_stock_loc_id
2608 wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
2610 #fetch customer and supplier locations, for references
2611 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context)
2613 #create in, out, internal picking types for warehouse
2614 input_loc = wh_input_stock_loc
2615 if warehouse.reception_steps == 'one_step':
2616 input_loc = wh_stock_loc
2617 output_loc = wh_output_stock_loc
2618 if warehouse.delivery_steps == 'ship_only':
2619 output_loc = wh_stock_loc
2621 #choose the next available color for the picking types of this warehouse
2622 all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
2623 not_used_colors = list(set(range(1, 10)) - set([x['color'] for x in all_used_colors]))
2624 color = not_used_colors and not_used_colors[0] or 1
2626 in_type_id = picking_type_obj.create(cr, uid, vals={
2627 'name': _('Receptions'),
2628 'warehouse_id': new_id,
2629 'code_id': 'incoming',
2630 'auto_force_assign': True,
2631 'sequence_id': in_seq_id,
2632 'default_location_src_id': supplier_loc.id,
2633 'default_location_dest_id': input_loc.id,
2634 'color': color}, context=context)
2635 out_type_id = picking_type_obj.create(cr, uid, vals={
2636 'name': _('Delivery Orders'),
2637 'warehouse_id': new_id,
2638 'code_id': 'outgoing',
2639 'sequence_id': out_seq_id,
2641 'default_location_src_id': output_loc.id,
2642 'default_location_dest_id': customer_loc.id,
2643 'color': color}, context=context)
2644 int_type_id = picking_type_obj.create(cr, uid, vals={
2645 'name': _('Internal Transfers'),
2646 'warehouse_id': new_id,
2647 'code_id': 'internal',
2648 'sequence_id': int_seq_id,
2649 'default_location_src_id': wh_stock_loc.id,
2650 'default_location_dest_id': wh_stock_loc.id,
2651 'active': reception_steps != 'one_step',
2653 'color': color}, context=context)
2654 pack_type_id = picking_type_obj.create(cr, uid, vals={
2656 'warehouse_id': new_id,
2657 'code_id': 'internal',
2658 'sequence_id': pack_seq_id,
2659 'default_location_src_id': wh_pack_stock_loc.id,
2660 'default_location_dest_id': output_loc.id,
2661 'active': delivery_steps == 'pick_pack_ship',
2663 'color': color}, context=context)
2664 pick_type_id = picking_type_obj.create(cr, uid, vals={
2666 'warehouse_id': new_id,
2667 'code_id': 'internal',
2668 'sequence_id': pick_seq_id,
2669 'default_location_src_id': wh_stock_loc.id,
2670 'default_location_dest_id': wh_pack_stock_loc.id,
2671 'active': delivery_steps != 'ship_only',
2673 'color': color}, context=context)
2675 #write picking types on WH
2677 'in_type_id': in_type_id,
2678 'out_type_id': out_type_id,
2679 'pack_type_id': pack_type_id,
2680 'pick_type_id': pick_type_id,
2681 'int_type_id': int_type_id,
2683 super(stock_warehouse, self).write(cr, uid, new_id, vals=vals, context=context)
2686 #create routes and push/pull rules
2687 new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
2688 self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
2691 def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
2692 return obj.name + ': ' + from_loc.name + ' -> ' + dest_loc.name
2694 def _format_routename(self, cr, uid, obj, name, context=None):
2695 return obj.name + ': ' + name
2697 def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
2698 #fetch customer and supplier locations, for references
2699 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
2702 'one_step': (_('Reception in 1 step'), []),
2703 'two_steps': (_('Reception in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2704 'three_steps': (_('Reception in 3 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2705 'crossdock': (_('Cross-Dock'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]),
2706 'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
2707 'pick_ship': (_('Pick + Ship'), [(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]),
2708 'pick_pack_ship': (_('Pick + Pack + Ship'), [(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]),
2711 def _handle_renaming(self, cr, uid, warehouse, name, context=None):
2712 location_obj = self.pool.get('stock.location')
2713 route_obj = self.pool.get('stock.location.route')
2714 pull_obj = self.pool.get('procurement.rule')
2715 push_obj = self.pool.get('stock.location.path')
2717 location_id = warehouse.lot_stock_id.location_id.id
2718 location_obj.write(cr, uid, location_id, {'name': name}, context=context)
2719 #rename route and push-pull rules
2720 for route in warehouse.route_ids:
2721 route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context)
2722 for pull in route.pull_ids:
2723 pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2724 for push in route.push_ids:
2725 push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2726 #change the mto pull rule name
2727 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
2729 def write(self, cr, uid, ids, vals, context=None):
2732 if isinstance(ids, (int, long)):
2734 seq_obj = self.pool.get('ir.sequence')
2735 route_obj = self.pool.get('stock.location.route')
2736 warehouse_obj = self.pool.get('stock.warehouse')
2738 context_with_inactive = context.copy()
2739 context_with_inactive['active_test'] = False
2740 for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
2741 #first of all, check if we need to delete and recreate route
2742 if vals.get('reception_steps') or vals.get('delivery_steps'):
2743 #activate and deactivate location according to reception and delivery option
2744 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
2745 # switch between route
2746 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
2747 if vals.get('code') or vals.get('name'):
2748 name = warehouse.name
2750 if vals.get('name'):
2751 name = vals.get('name')
2752 self._handle_renaming(cr, uid, warehouse, name, context=context_with_inactive)
2753 seq_obj.write(cr, uid, warehouse.in_type_id.sequence_id.id, {'name': name + _(' Sequence in'), 'prefix': vals.get('code', warehouse.code) + '\IN\\'}, context=context)
2754 seq_obj.write(cr, uid, warehouse.out_type_id.sequence_id.id, {'name': name + _(' Sequence out'), 'prefix': vals.get('code', warehouse.code) + '\OUT\\'}, context=context)
2755 seq_obj.write(cr, uid, warehouse.pack_type_id.sequence_id.id, {'name': name + _(' Sequence packing'), 'prefix': vals.get('code', warehouse.code) + '\PACK\\'}, context=context)
2756 seq_obj.write(cr, uid, warehouse.pick_type_id.sequence_id.id, {'name': name + _(' Sequence picking'), 'prefix': vals.get('code', warehouse.code) + '\PICK\\'}, context=context)
2757 seq_obj.write(cr, uid, warehouse.int_type_id.sequence_id.id, {'name': name + _(' Sequence internal'), 'prefix': vals.get('code', warehouse.code) + '\INT\\'}, context=context)
2758 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
2759 for cmd in vals.get('resupply_wh_ids'):
2761 new_ids = set(cmd[2])
2762 old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
2763 to_add_wh_ids = new_ids - old_ids
2765 supplier_warehouses = warehouse_obj.browse(cr, uid, list(to_add_wh_ids), context=context)
2766 self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
2767 to_remove_wh_ids = old_ids - new_ids
2768 if to_remove_wh_ids:
2769 to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', 'in', list(to_remove_wh_ids))], context=context)
2770 if to_remove_route_ids:
2771 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
2775 if 'default_resupply_wh_id' in vals:
2776 if warehouse.default_resupply_wh_id:
2777 #remove the existing resupplying route on all products
2778 to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', warehouse.default_resupply_wh_id.id)], context=context)
2779 for inter_wh_route_id in to_remove_route_ids:
2780 self._unassign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2781 if vals.get('default_resupply_wh_id'):
2782 #assign the new resupplying route on all products
2783 to_assign_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', vals.get('default_resupply_wh_id'))], context=context)
2784 for inter_wh_route_id in to_assign_route_ids:
2785 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2787 return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
2789 def unlink(self, cr, uid, ids, context=None):
2790 #TODO try to delete location and route and if not possible, put them in inactive
2791 return super(stock_warehouse, self).unlink(cr, uid, ids, context=context)
2793 def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
2794 all_routes = [route.id for route in warehouse.route_ids]
2795 all_routes += [warehouse.mto_pull_id.route_id.id]
2798 def view_all_routes_for_wh(self, cr, uid, ids, context=None):
2800 for wh in self.browse(cr, uid, ids, context=context):
2801 all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
2803 domain = [('id', 'in', all_routes)]
2805 'name': _('Warehouse\'s Routes'),
2807 'res_model': 'stock.location.route',
2808 'type': 'ir.actions.act_window',
2810 'view_mode': 'tree,form',
2811 'view_type': 'form',
2815 class stock_location_path(osv.osv):
2816 _name = "stock.location.path"
2817 _description = "Pushed Flows"
2820 def _get_route(self, cr, uid, ids, context=None):
2821 #WARNING TODO route_id is not required, so a field related seems a bad idea >-<
2827 context_with_inactive = context.copy()
2828 context_with_inactive['active_test'] = False
2829 for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive):
2830 for push_rule in route.push_ids:
2831 result[push_rule.id] = True
2832 return result.keys()
2835 'name': fields.char('Operation Name', size=64, required=True),
2836 'company_id': fields.many2one('res.company', 'Company'),
2837 'route_id': fields.many2one('stock.location.route', 'Route'),
2838 'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
2839 'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
2840 'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
2841 'invoice_state': fields.selection([
2842 ("invoiced", "Invoiced"),
2843 ("2binvoiced", "To Be Invoiced"),
2844 ("none", "Not Applicable")], "Invoice Status",
2846 'picking_type_id': fields.many2one('stock.picking.type', 'Type of the new Operation', required=True, help="This is the picking type associated with the different pickings"),
2847 'auto': fields.selection(
2848 [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
2850 required=True, select=1,
2851 help="This is used to define paths the product has to follow within the location tree.\n" \
2852 "The 'Automatic Move' value will create a stock move after the current one that will be "\
2853 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
2854 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
2856 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move is cancelled or split, the move generated by this move will too'),
2857 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={
2858 'stock.location.route': (_get_route, ['active'], 20),
2859 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),},
2860 help="If the active field is set to False, it will allow you to hide the rule without removing it." ),
2861 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
2866 'invoice_state': 'none',
2867 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
2871 def _apply(self, cr, uid, rule, move, context=None):
2872 move_obj = self.pool.get('stock.move')
2873 newdate = (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta.relativedelta(days=rule.delay or 0)).strftime('%Y-%m-%d')
2874 if rule.auto=='transparent':
2875 move_obj.write(cr, uid, [move.id], {
2877 'location_dest_id': rule.location_dest_id.id
2879 if rule.location_dest_id.id<>move.location_dest_id.id:
2880 move_obj._push_apply(self, cr, uid, move.id, context)
2883 move_id = move_obj.copy(cr, uid, move.id, {
2884 'location_id': move.location_dest_id.id,
2885 'location_dest_id': rule.location_dest_id.id,
2886 'date': datetime.now().strftime('%Y-%m-%d'),
2887 'company_id': rule.company_id and rule.company_id.id or False,
2888 'date_expected': newdate,
2889 'picking_id': False,
2890 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
2892 'propagate': rule.propagate,
2893 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
2895 move_obj.write(cr, uid, [move.id], {
2896 'move_dest_id': move_id,
2898 move_obj.action_confirm(cr, uid, [move_id], context=None)
2901 class stock_move_putaway(osv.osv):
2902 _name = 'stock.move.putaway'
2903 _description = 'Proposed Destination'
2905 'move_id': fields.many2one('stock.move', required=True),
2906 'location_id': fields.many2one('stock.location', 'Location', required=True),
2907 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
2908 'quantity': fields.float('Quantity', required=True),
2913 # -------------------------
2914 # Packaging related stuff
2915 # -------------------------
2917 from openerp.report import report_sxw
2918 report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package', 'addons/stock/report/picking_barcode.rml')
2920 class stock_package(osv.osv):
2922 These are the packages, containing quants and/or other packages
2924 _name = "stock.quant.package"
2925 _description = "Physical Packages"
2926 _parent_name = "parent_id"
2927 _parent_store = True
2928 _parent_order = 'name'
2929 _order = 'parent_left'
2931 def name_get(self, cr, uid, ids, context=None):
2932 res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
2935 def _complete_name(self, cr, uid, ids, name, args, context=None):
2936 """ Forms complete name of location from parent location to child location.
2937 @return: Dictionary of values
2940 for m in self.browse(cr, uid, ids, context=context):
2942 parent = m.parent_id
2944 res[m.id] = parent.name + ' / ' + res[m.id]
2945 parent = parent.parent_id
2948 def _get_packages(self, cr, uid, ids, context=None):
2949 """Returns packages from quants for store"""
2951 for quant in self.browse(cr, uid, ids, context=context):
2952 if quant.package_id:
2953 res.add(quant.package_id.id)
2956 def _get_packages_to_relocate(self, cr, uid, ids, context=None):
2958 for pack in self.browse(cr, uid, ids, context=context):
2961 res.add(pack.parent_id.id)
2964 # TODO: Problem when package is empty!
2966 def _get_package_info(self, cr, uid, ids, name, args, context=None):
2967 default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
2968 res = {}.fromkeys(ids, {'location_id': False, 'company_id': default_company_id})
2969 for pack in self.browse(cr, uid, ids, context=context):
2971 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
2972 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
2973 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
2974 elif pack.children_ids:
2975 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
2976 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
2977 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
2981 'name': fields.char('Package Reference', size=64, select=True),
2982 'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
2983 'parent_left': fields.integer('Left Parent', select=1),
2984 'parent_right': fields.integer('Right Parent', select=1),
2985 'packaging_id': fields.many2one('product.packaging', 'Type of Packaging'),
2986 'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
2988 'stock.quant': (_get_packages, ['location_id'], 10),
2989 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
2991 'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content'),
2992 'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict'),
2993 'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages'),
2994 'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package",
2996 'stock.quant': (_get_packages, ['company_id'], 10),
2997 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
2999 'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
3001 'stock.quant': (_get_packages, ['owner_id'], 10),
3002 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3006 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3008 def _check_location(self, cr, uid, ids, context=None):
3009 '''checks that all quants in a package are stored in the same location'''
3010 quant_obj = self.pool.get('stock.quant')
3011 for pack in self.browse(cr, uid, ids, context=context):
3013 while parent.parent_id:
3014 parent = parent.parent_id
3015 quant_ids = self.get_content(cr, uid, [parent.id], context=context)
3016 quants = quant_obj.browse(cr, uid, quant_ids, context=context)
3017 location_id = quants and quants[0].location_id.id or False
3018 if not all([quant.location_id.id == location_id for quant in quants]):
3023 (_check_location, 'Everything inside a package should be in the same location', ['location_id']),
3026 def action_print(self, cr, uid, ids, context=None):
3030 'ids': context.get('active_id') and [context.get('active_id')] or ids,
3031 'model': 'stock.quant.package',
3032 'form': self.read(cr, uid, ids)[0]
3035 'type': 'ir.actions.report.xml',
3036 'report_name': 'stock.quant.package.barcode',
3040 def unpack(self, cr, uid, ids, context=None):
3041 quant_obj = self.pool.get('stock.quant')
3042 for package in self.browse(cr, uid, ids, context=context):
3043 quant_ids = [quant.id for quant in package.quant_ids]
3044 quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3045 children_package_ids = [child_package.id for child_package in package.children_ids]
3046 self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3047 #delete current package since it contains nothing anymore
3048 self.unlink(cr, uid, ids, context=context)
3049 return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3051 def get_content(self, cr, uid, ids, context=None):
3052 child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3053 return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3055 def get_content_package(self, cr, uid, ids, context=None):
3056 quants_ids = self.get_content(cr, uid, ids, context=context)
3057 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3058 res['domain'] = [('id', 'in', quants_ids)]
3061 def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3062 ''' find the total of given product 'product_id' inside the given package 'package_id'''
3063 quant_obj = self.pool.get('stock.quant')
3064 all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3066 for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3067 if quant.product_id.id == product_id:
3072 class stock_pack_operation(osv.osv):
3073 _name = "stock.pack.operation"
3074 _description = "Packing Operation"
3076 def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3078 for ops in self.browse(cr, uid, ids, context=context):
3079 qty = ops.product_qty
3080 for quant in ops.reserved_quant_ids:
3086 'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3087 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
3088 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3089 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3090 'package_id': fields.many2one('stock.quant.package', 'Package'), # 2
3091 'quant_id': fields.many2one('stock.quant', 'Quant'), # 3
3092 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3093 'result_package_id': fields.many2one('stock.quant.package', 'Container Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3094 'date': fields.datetime('Date', required=True),
3095 'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3096 #'update_cost': fields.boolean('Need cost update'),
3097 'cost': fields.float("Cost", help="Unit Cost for this product line"),
3098 'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3099 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_op_id', string='Reserved Quants', readonly=True, help='Quants reserved for this operation'),
3100 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'),
3104 'date': fields.date.context_today,
3108 def _get_domain(self, cr, uid, ops, context=None):
3110 Gives domain for different
3114 res.append(('package_id', '=', ops.package_id.id), )
3116 res.append(('lot_id', '=', ops.lot_id.id), )
3118 res.append(('owner_id', '=', ops.owner_id.id), )
3120 res.append(('owner_id', '=', False), )
3123 #TODO: this function can be refactored
3124 def _search_and_increment(self, cr, uid, picking_id, key, context=None):
3125 '''Search for an operation on an existing key in a picking, if it exists increment the qty (+1) otherwise create it
3127 :param key: tuple directly reusable in a domain
3128 context can receive a key 'current_package_id' with the package to consider for this operation
3131 previously: returns the update to do in stock.move one2many field of picking (adapt remaining quantities) and to the list of package in the classic one2many syntax
3132 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
3133 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
3134 (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
3136 quant_obj = self.pool.get('stock.quant')
3140 #if current_package_id is given in the context, we increase the number of items in this package
3141 package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3142 existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id), key] + package_clause, context=context)
3143 if existing_operation_ids:
3144 #existing operation found for the given key and picking => increment its quantity
3145 operation_id = existing_operation_ids[0]
3146 qty = self.browse(cr, uid, operation_id, context=context).product_qty + 1
3147 self.write(cr, uid, operation_id, {'product_qty': qty}, context=context)
3149 #no existing operation found for the given key and picking => create a new one
3150 var_name, dummy, value = key
3152 if var_name == 'product_id':
3153 uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
3154 elif var_name == 'quant_id':
3155 quant = quant_obj.browse(cr, uid, value, context=context)
3156 uom_id = quant.product_id.uom_id.id
3158 'picking_id': picking_id,
3161 'product_uom_id': uom_id,
3163 operation_id = self.create(cr, uid, values, context=context)
3164 values.update({'id': operation_id})
3167 class stock_warehouse_orderpoint(osv.osv):
3169 Defines Minimum stock rules.
3171 _name = "stock.warehouse.orderpoint"
3172 _description = "Minimum Inventory Rule"
3174 def _get_draft_procurements(self, cr, uid, ids, field_name, arg, context=None):
3178 procurement_obj = self.pool.get('procurement.order')
3179 for orderpoint in self.browse(cr, uid, ids, context=context):
3180 procurement_ids = procurement_obj.search(cr, uid, [('state', '=', 'draft'), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)])
3181 result[orderpoint.id] = procurement_ids
3184 def _check_product_uom(self, cr, uid, ids, context=None):
3186 Check if the UoM has the same category as the product standard UoM
3191 for rule in self.browse(cr, uid, ids, context=context):
3192 if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
3198 'name': fields.char('Name', size=32, required=True),
3199 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
3200 'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
3201 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
3202 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
3203 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '!=', 'service')]),
3204 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
3205 'product_min_qty': fields.float('Minimum Quantity', required=True,
3206 help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
3207 "a procurement to bring the forecasted quantity to the Max Quantity."),
3208 'product_max_qty': fields.float('Maximum Quantity', required=True,
3209 help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
3210 "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
3211 'qty_multiple': fields.integer('Qty Multiple', required=True,
3212 help="The procurement quantity will be rounded up to this multiple."),
3213 'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
3214 'company_id': fields.many2one('res.company', 'Company', required=True),
3215 'procurement_draft_ids': fields.function(_get_draft_procurements, type='many2many', relation="procurement.order", \
3216 string="Related Procurement Orders", help="Draft procurement of the product and location of that orderpoint"),
3219 'active': lambda *a: 1,
3220 'logic': lambda *a: 'max',
3221 'qty_multiple': lambda *a: 1,
3222 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3223 'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
3224 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
3226 _sql_constraints = [
3227 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
3230 (_check_product_uom, 'You have to select a product unit of measure in the same category than the default unit of measure of the product', ['product_id', 'product_uom']),
3233 def default_get(self, cr, uid, fields, context=None):
3234 res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
3235 # default 'warehouse_id' and 'location_id'
3236 if 'warehouse_id' not in res:
3237 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0', context)
3238 res['warehouse_id'] = warehouse.id
3239 if 'location_id' not in res:
3240 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, res['warehouse_id'], context)
3241 res['location_id'] = warehouse.lot_stock_id.id
3244 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
3245 """ Finds location id for changed warehouse.
3246 @param warehouse_id: Changed id of warehouse.
3247 @return: Dictionary of values.
3250 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
3251 v = {'location_id': w.lot_stock_id.id}
3255 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
3256 """ Finds UoM for changed product.
3257 @param product_id: Changed id of product.
3258 @return: Dictionary of values.
3261 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3262 d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
3263 v = {'product_uom': prod.uom_id.id}
3264 return {'value': v, 'domain': d}
3265 return {'domain': {'product_uom': []}}
3267 def copy(self, cr, uid, id, default=None, context=None):
3271 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3273 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
3277 class stock_picking_type(osv.osv):
3278 _name = "stock.picking.type"
3279 _description = "The picking type determines the picking view"
3281 def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
3282 """ Generic method to generate data for bar chart values using SparklineBarWidget.
3283 This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
3285 :param obj: the target model (i.e. crm_lead)
3286 :param domain: the domain applied to the read_group
3287 :param list read_fields: the list of fields to read in the read_group
3288 :param str value_field: the field used to compute the value of the bar slice
3289 :param str groupby_field: the fields used to group
3291 :return list section_result: a list of dicts: [
3292 { 'value': (int) bar_column_value,
3293 'tootip': (str) bar_column_tooltip,
3297 month_begin = date.today().replace(day=1)
3300 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
3301 } for i in range(10, -1, -1)]
3302 group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
3303 for group in group_obj:
3304 group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
3305 month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
3306 section_result[10 - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
3307 return section_result
3309 def _get_picking_data(self, cr, uid, ids, field_name, arg, context=None):
3310 obj = self.pool.get('stock.picking')
3311 res = dict.fromkeys(ids, False)
3312 month_begin = date.today().replace(day=1)
3313 groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3314 groupby_end = (month_begin + relativedelta.relativedelta(months=3)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3317 ('picking_type_id', '=', id),
3318 ('state', 'not in', ['done', 'cancel']),
3319 ('date', '>=', groupby_begin),
3320 ('date', '<', groupby_end),
3322 res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['date'], 'picking_type_id_count', 'date', context=context)
3325 def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
3326 obj = self.pool.get('stock.picking')
3328 'count_picking_draft': [('state', '=', 'draft')],
3329 'count_picking_waiting': [('state','=','confirmed')],
3330 'count_picking_ready': [('state','=','assigned')],
3331 'count_picking': [('state','in',('assigned','waiting','confirmed'))],
3332 'count_picking_late': [('min_date','<', time.strftime('%Y-%m-%d %H:%M:%S')), ('state','in',('assigned','waiting','confirmed'))],
3333 'count_picking_backorders': [('backorder_id','<>', False), ('state','!=','done')],
3336 for field in domains:
3337 data = obj.read_group(cr, uid, domains[field] +
3338 [('state', 'not in',('done','cancel')), ('picking_type_id', 'in', ids)],
3339 ['picking_type_id'], ['picking_type_id'], context=context)
3340 count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
3342 result.setdefault(tid, {})[field] = count.get(tid, 0)
3344 if result[tid]['count_picking']:
3345 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] *100 / result[tid]['count_picking']
3346 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] *100 / (result[tid]['count_picking'] + result[tid]['count_picking_draft'])
3348 result[tid]['rate_picking_late'] = 0
3349 result[tid]['rate_picking_backorders'] = 0
3352 #TODO: not returning valus in required format to show in sparkline library,just added latest_picking_waiting need to add proper logic.
3353 def _get_picking_history(self, cr, uid, ids, field_names, arg, context=None):
3354 obj = self.pool.get('stock.picking')
3358 'latest_picking_late': [],
3359 'latest_picking_backorders': [],
3360 'latest_picking_waiting': []
3363 pick_ids = obj.search(cr, uid, [('state', '=','done'), ('picking_type_id','=',type_id)], limit=12, order="date desc", context=context)
3364 for pick in obj.browse(cr, uid, pick_ids, context=context):
3365 result[type_id]['latest_picking_late'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3366 result[type_id]['latest_picking_backorders'] = bool(pick.backorder_id)
3367 result[type_id]['latest_picking_waiting'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3370 def onchange_picking_code(self, cr, uid, ids, picking_code=False):
3371 if not picking_code:
3374 obj_data = self.pool.get('ir.model.data')
3375 stock_loc = obj_data.get_object_reference(cr, uid, 'stock','stock_location_stock')[1]
3378 'default_location_src_id': stock_loc,
3379 'default_location_dest_id': stock_loc,
3381 if picking_code == 'incoming':
3382 result['default_location_src_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_suppliers')[1]
3383 return {'value': result}
3384 if picking_code == 'outgoing':
3385 result['default_location_dest_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_customers')[1]
3386 return {'value': result}
3388 return {'value': result}
3390 def _get_name(self, cr, uid, ids, field_names, arg, context=None):
3391 return dict(self.name_get(cr, uid, ids, context=context))
3393 def name_get(self, cr, uid, ids, context=None):
3394 """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
3397 if not isinstance(ids, list):
3402 for record in self.browse(cr, uid, ids, context=context):
3404 if record.warehouse_id:
3405 name = record.warehouse_id.name + ': ' +name
3406 if context.get('special_shortened_wh_name'):
3407 if record.warehouse_id:
3408 name = record.warehouse_id.name
3410 name = _('Customer') + ' (' + record.name + ')'
3411 res.append((record.id, name))
3414 def _default_warehouse(self, cr, uid, context=None):
3415 user = self.pool.get('res.users').browse(cr, uid, uid, context)
3416 res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
3417 return res and res[0] or False
3420 'name': fields.char('name', translate=True, required=True),
3421 'complete_name': fields.function(_get_name, type='char', string='Name'),
3422 'pack': fields.boolean('Prefill Pack Operations', help='This picking type needs packing interface'),
3423 'auto_force_assign': fields.boolean('Automatic Availability', help='This picking type does\'t need to check for the availability in source location.'),
3424 'color': fields.integer('Color Index'),
3425 'delivery': fields.boolean('Print delivery'),
3426 'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
3427 'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
3428 'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
3429 #TODO: change field name to "code" as it's not a many2one anymore
3430 'code_id': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Picking type code', required=True),
3431 'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
3432 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
3433 'active': fields.boolean('Active'),
3435 # Statistics for the kanban view
3436 'weekly_picking': fields.function(_get_picking_data,
3438 string='Scheduled pickings per week'),
3440 'count_picking_draft': fields.function(_get_picking_count,
3441 type='integer', multi='_get_picking_count'),
3442 'count_picking_ready': fields.function(_get_picking_count,
3443 type='integer', multi='_get_picking_count'),
3444 'count_picking': fields.function(_get_picking_count,
3445 type='integer', multi='_get_picking_count'),
3446 'count_picking_waiting': fields.function(_get_picking_count,
3447 type='integer', multi='_get_picking_count'),
3448 'count_picking_late': fields.function(_get_picking_count,
3449 type='integer', multi='_get_picking_count'),
3450 'count_picking_backorders': fields.function(_get_picking_count,
3451 type='integer', multi='_get_picking_count'),
3453 'rate_picking_late': fields.function(_get_picking_count,
3454 type='integer', multi='_get_picking_count'),
3455 'rate_picking_backorders': fields.function(_get_picking_count,
3456 type='integer', multi='_get_picking_count'),
3458 'latest_picking_late': fields.function(_get_picking_history,
3459 type='string', multi='_get_picking_history'),
3460 'latest_picking_backorders': fields.function(_get_picking_history,
3461 type='string', multi='_get_picking_history'),
3462 'latest_picking_waiting': fields.function(_get_picking_history,
3463 type='string', multi='_get_picking_history'),
3467 'warehouse_id': _default_warehouse,
3472 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: