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 _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2268 'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2269 'warehouse_selectable': False,
2270 'product_selectable': True,
2271 'product_categ_selectable': True,
2272 'supplied_wh_id': warehouse.id,
2273 'supplier_wh_id': wh.id,
2276 def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2277 location_obj = self.pool.get('stock.location')
2278 route_obj = self.pool.get('stock.location.route')
2279 pull_obj = self.pool.get('procurement.rule')
2280 #create route selectable on the product to resupply the warehouse from another one
2281 inter_wh_location_id = self._get_inter_wh_location(cr, uid, warehouse, context=context)
2282 if inter_wh_location_id:
2283 input_loc = warehouse.wh_input_stock_loc_id
2284 if warehouse.reception_steps == 'one_step':
2285 input_loc = warehouse.lot_stock_id
2286 inter_wh_location = location_obj.browse(cr, uid, inter_wh_location_id, context=context)
2287 for wh in supplier_warehouses:
2288 output_loc = wh.wh_output_stock_loc_id
2289 if wh.delivery_steps == 'ship_only':
2290 output_loc = wh.lot_stock_id
2291 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2292 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2293 values = [(output_loc, inter_wh_location, wh.out_type_id.id), (inter_wh_location, input_loc, warehouse.in_type_id.id)]
2294 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, inter_wh_route_id, context=context)
2295 for pull_rule in pull_rules_list:
2296 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2297 #if the warehouse is also set as default resupply method, assign this route automatically to all product
2298 if default_resupply_wh and default_resupply_wh.id == wh.id:
2299 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2301 def _default_stock_id(self, cr, uid, context=None):
2302 #lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock')
2304 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2305 return warehouse.lot_stock_id.id
2310 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2311 'lot_stock_id': _default_stock_id,
2312 'reception_steps': 'one_step',
2313 'delivery_steps': 'ship_only',
2315 _sql_constraints = [
2316 ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2317 ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2318 ('default_resupply_wh_diff', 'check (id != default_resupply_wh_id)', 'The default resupply warehouse should be different that the warehouse itself!'),
2321 def _get_partner_locations(self, cr, uid, ids, context=None):
2322 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2323 data_obj = self.pool.get('ir.model.data')
2324 location_obj = self.pool.get('stock.location')
2326 customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2327 supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2329 customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2330 customer_loc = customer_loc and customer_loc[0] or False
2331 supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2332 supplier_loc = supplier_loc and supplier_loc[0] or False
2333 if not (customer_loc and supplier_loc):
2334 raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2335 return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2337 def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2338 location_obj = self.pool.get('stock.location')
2340 new_reception_step = new_reception_step or warehouse.reception_steps
2341 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2342 if warehouse.reception_steps != new_reception_step:
2343 location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2344 if new_reception_step != 'one_step':
2345 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2346 if new_reception_step == 'three_steps':
2347 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2349 if warehouse.delivery_steps != new_delivery_step:
2350 location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
2351 if new_delivery_step != 'ship_only':
2352 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
2353 if new_delivery_step == 'pick_pack_ship':
2354 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
2357 def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
2359 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2360 'product_categ_selectable': True,
2361 'product_selectable': False,
2365 def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
2367 push_rules_list = []
2368 pull_rules_list = []
2369 for from_loc, dest_loc, pick_type_id in values:
2370 push_rules_list.append({
2371 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2372 'location_from_id': from_loc.id,
2373 'location_dest_id': dest_loc.id,
2374 'route_id': new_route_id,
2376 'picking_type_id': pick_type_id,
2378 'warehouse_id': warehouse.id,
2380 pull_rules_list.append({
2381 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2382 'location_src_id': from_loc.id,
2383 'location_id': dest_loc.id,
2384 'route_id': new_route_id,
2386 'picking_type_id': pick_type_id,
2387 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
2389 'warehouse_id': warehouse.id,
2392 return push_rules_list, pull_rules_list
2394 def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
2395 route_obj = self.pool.get('stock.location.route')
2396 data_obj = self.pool.get('ir.model.data')
2398 mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
2400 mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('MTO'))], context=context)
2401 mto_route_id = mto_route_id and mto_route_id[0] or False
2402 if not mto_route_id:
2403 raise osv.except_osv(_('Error!'), _('Can\'t find any generic MTO route.'))
2405 from_loc, dest_loc, pick_type_id = values[0]
2407 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
2408 'location_src_id': from_loc.id,
2409 'location_id': dest_loc.id,
2410 'route_id': mto_route_id,
2412 'picking_type_id': pick_type_id,
2413 'procure_method': 'make_to_order',
2415 'warehouse_id': warehouse.id,
2418 def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
2420 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2421 'warehouse_selectable': False,
2422 'product_selectable': True,
2423 'product_categ_selectable': True,
2424 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
2428 def create_routes(self, cr, uid, ids, warehouse, context=None):
2430 route_obj = self.pool.get('stock.location.route')
2431 pull_obj = self.pool.get('procurement.rule')
2432 push_obj = self.pool.get('stock.location.path')
2433 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2434 #create reception route and rules
2435 route_name, values = routes_dict[warehouse.reception_steps]
2436 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2437 reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
2438 wh_route_ids.append((4, reception_route_id))
2439 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
2440 #create the push/pull rules
2441 for push_rule in push_rules_list:
2442 push_obj.create(cr, uid, vals=push_rule, context=context)
2443 for pull_rule in pull_rules_list:
2444 #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
2445 pull_rule['procure_method'] = 'make_to_order'
2446 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2448 #create MTS route and pull rules for delivery a specific route MTO to be set on the product
2449 route_name, values = routes_dict[warehouse.delivery_steps]
2450 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2451 #create the route and its pull rules
2452 delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
2453 wh_route_ids.append((4, delivery_route_id))
2454 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
2455 for pull_rule in pull_rules_list:
2456 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2457 #create MTO pull rule and link it to the generic MTO route
2458 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2459 mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
2461 #create a route for cross dock operations, that can be set on products and product categories
2462 route_name, values = routes_dict['crossdock']
2463 crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
2464 crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
2465 wh_route_ids.append((4, crossdock_route_id))
2466 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)
2467 for pull_rule in pull_rules_list:
2468 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2470 #create route selectable on the product to resupply the warehouse from another one
2471 self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
2473 #return routes and mto pull rule for warehouse
2475 'route_ids': wh_route_ids,
2476 'mto_pull_id': mto_pull_id,
2477 'reception_route_id': reception_route_id,
2478 'delivery_route_id': delivery_route_id,
2479 'crossdock_route_id': crossdock_route_id,
2482 def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2483 picking_type_obj = self.pool.get('stock.picking.type')
2484 pull_obj = self.pool.get('procurement.rule')
2485 push_obj = self.pool.get('stock.location.path')
2486 route_obj = self.pool.get('stock.location.route')
2487 new_reception_step = new_reception_step or warehouse.reception_steps
2488 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2490 #change the default source and destination location and (de)activate picking types
2491 input_loc = warehouse.wh_input_stock_loc_id
2492 if new_reception_step == 'one_step':
2493 input_loc = warehouse.lot_stock_id
2494 output_loc = warehouse.wh_output_stock_loc_id
2495 if new_delivery_step == 'ship_only':
2496 output_loc = warehouse.lot_stock_id
2497 picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
2498 picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
2499 picking_type_obj.write(cr, uid, warehouse.int_type_id.id, {'active': new_reception_step != 'one_step'}, context=context)
2500 picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
2501 picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
2503 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2504 #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
2505 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
2506 route_name, values = routes_dict[new_delivery_step]
2507 route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2508 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
2509 #create the pull rules
2510 for pull_rule in pull_rules_list:
2511 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2513 #update reception route and rules: unlink the existing rules of the warehouse reception route and recreate it
2514 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
2515 push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
2516 route_name, values = routes_dict[new_reception_step]
2517 route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2518 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
2519 #create the push/pull rules
2520 for push_rule in push_rules_list:
2521 push_obj.create(cr, uid, vals=push_rule, context=context)
2522 for pull_rule in pull_rules_list:
2523 #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
2524 pull_rule['procure_method'] = 'make_to_order'
2525 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2527 route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
2530 dummy, values = routes_dict[new_delivery_step]
2531 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2532 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
2535 def create(self, cr, uid, vals, context=None):
2540 data_obj = self.pool.get('ir.model.data')
2541 seq_obj = self.pool.get('ir.sequence')
2542 picking_type_obj = self.pool.get('stock.picking.type')
2543 location_obj = self.pool.get('stock.location')
2545 #create view location for warehouse
2546 wh_loc_id = location_obj.create(cr, uid, {
2547 'name': _(vals.get('name')),
2549 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1]
2551 vals['view_location_id'] = wh_loc_id
2552 #create all location
2553 reception_steps = vals.get('reception_steps', False)
2554 delivery_steps = vals.get('delivery_steps', False)
2555 context_with_inactive = context.copy()
2556 context_with_inactive['active_test'] = False
2558 {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
2559 {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
2560 {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
2561 {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
2562 {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
2564 for values in sub_locations:
2565 location_id = location_obj.create(cr, uid, {
2566 'name': values['name'],
2567 'usage': 'internal',
2568 'location_id': wh_loc_id,
2569 'active': values['active'],
2570 }, context=context_with_inactive)
2571 vals[values['field']] = location_id
2573 #create new sequences
2574 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)
2575 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)
2576 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)
2577 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)
2578 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)
2581 new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
2583 warehouse = self.browse(cr, uid, new_id, context=context)
2584 wh_stock_loc = warehouse.lot_stock_id
2585 wh_input_stock_loc = warehouse.wh_input_stock_loc_id
2586 wh_output_stock_loc = warehouse.wh_output_stock_loc_id
2587 wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
2589 #fetch customer and supplier locations, for references
2590 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context)
2592 #create in, out, internal picking types for warehouse
2593 input_loc = wh_input_stock_loc
2594 if warehouse.reception_steps == 'one_step':
2595 input_loc = wh_stock_loc
2596 output_loc = wh_output_stock_loc
2597 if warehouse.delivery_steps == 'ship_only':
2598 output_loc = wh_stock_loc
2600 #choose the next available color for the picking types of this warehouse
2601 all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
2602 not_used_colors = list(set(range(1, 10)) - set([x['color'] for x in all_used_colors]))
2603 color = not_used_colors and not_used_colors[0] or 1
2605 in_type_id = picking_type_obj.create(cr, uid, vals={
2606 'name': _('Receptions'),
2607 'warehouse_id': new_id,
2608 'code_id': 'incoming',
2609 'auto_force_assign': True,
2610 'sequence_id': in_seq_id,
2611 'default_location_src_id': supplier_loc.id,
2612 'default_location_dest_id': input_loc.id,
2613 'color': color}, context=context)
2614 out_type_id = picking_type_obj.create(cr, uid, vals={
2615 'name': _('Delivery Orders'),
2616 'warehouse_id': new_id,
2617 'code_id': 'outgoing',
2618 'sequence_id': out_seq_id,
2620 'default_location_src_id': output_loc.id,
2621 'default_location_dest_id': customer_loc.id,
2622 'color': color}, context=context)
2623 int_type_id = picking_type_obj.create(cr, uid, vals={
2624 'name': _('Internal Transfers'),
2625 'warehouse_id': new_id,
2626 'code_id': 'internal',
2627 'sequence_id': int_seq_id,
2628 'default_location_src_id': wh_stock_loc.id,
2629 'default_location_dest_id': wh_stock_loc.id,
2630 'active': reception_steps != 'one_step',
2632 'color': color}, context=context)
2633 pack_type_id = picking_type_obj.create(cr, uid, vals={
2635 'warehouse_id': new_id,
2636 'code_id': 'internal',
2637 'sequence_id': pack_seq_id,
2638 'default_location_src_id': wh_pack_stock_loc.id,
2639 'default_location_dest_id': output_loc.id,
2640 'active': delivery_steps == 'pick_pack_ship',
2642 'color': color}, context=context)
2643 pick_type_id = picking_type_obj.create(cr, uid, vals={
2645 'warehouse_id': new_id,
2646 'code_id': 'internal',
2647 'sequence_id': pick_seq_id,
2648 'default_location_src_id': wh_stock_loc.id,
2649 'default_location_dest_id': wh_pack_stock_loc.id,
2650 'active': delivery_steps != 'ship_only',
2652 'color': color}, context=context)
2654 #write picking types on WH
2656 'in_type_id': in_type_id,
2657 'out_type_id': out_type_id,
2658 'pack_type_id': pack_type_id,
2659 'pick_type_id': pick_type_id,
2660 'int_type_id': int_type_id,
2662 super(stock_warehouse, self).write(cr, uid, new_id, vals=vals, context=context)
2665 #create routes and push/pull rules
2666 new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
2667 self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
2670 def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
2671 return obj.name + ': ' + from_loc.name + ' -> ' + dest_loc.name
2673 def _format_routename(self, cr, uid, obj, name, context=None):
2674 return obj.name + ': ' + name
2676 def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
2677 #fetch customer and supplier locations, for references
2678 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
2681 'one_step': (_('Reception in 1 step'), []),
2682 'two_steps': (_('Reception in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2683 '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)]),
2684 '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)]),
2685 'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
2686 '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)]),
2687 '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)]),
2690 def write(self, cr, uid, ids, vals, context=None):
2693 if isinstance(ids, (int, long)):
2695 seq_obj = self.pool.get('ir.sequence')
2696 location_obj = self.pool.get('stock.location')
2697 route_obj = self.pool.get('stock.location.route')
2698 warehouse_obj = self.pool.get('stock.warehouse')
2699 pull_obj = self.pool.get('procurement.rule')
2700 push_obj = self.pool.get('stock.location.path')
2702 context_with_inactive = context.copy()
2703 context_with_inactive['active_test'] = False
2704 for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
2705 #first of all, check if we need to delete and recreate route
2706 if vals.get('reception_steps') or vals.get('delivery_steps'):
2707 #activate and deactivate location according to reception and delivery option
2708 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
2709 # switch between route
2710 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
2711 if vals.get('code') or vals.get('name'):
2712 name = warehouse.name
2714 if vals.get('name'):
2715 name = vals.get('name')
2717 location_id = warehouse.lot_stock_id.location_id.id
2718 location_obj.write(cr, uid, location_id, {'name': name}, context=context_with_inactive)
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_with_inactive)
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_with_inactive)
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_with_inactive)
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_with_inactive)
2728 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)
2729 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)
2730 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)
2731 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)
2732 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)
2734 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
2735 for cmd in vals.get('resupply_wh_ids'):
2737 new_ids = set(cmd[2])
2738 old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
2739 to_add_wh_ids = new_ids - old_ids
2740 supplier_warehouses = warehouse_obj.browse(cr, uid, list(to_add_wh_ids), context=context)
2741 self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
2742 to_remove_wh_ids = old_ids - new_ids
2743 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)
2744 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
2748 if 'default_resupply_wh_id' in vals:
2749 if warehouse.default_resupply_wh_id:
2750 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)
2751 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
2752 self._create_resupply_routes(cr, uid, warehouse, [warehouse.default_resupply_wh_id], False, context=context)
2753 if vals.get('default_resupply_wh_id'):
2754 to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', vals.get('default_resupply_wh_id'))], context=context)
2755 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
2756 def_supplier_wh = warehouse_obj.browse(cr, uid, vals['default_resupply_wh_id'], context=context)
2757 self._create_resupply_routes(cr, uid, warehouse, [def_supplier_wh], def_supplier_wh, context=context)
2759 return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
2761 def unlink(self, cr, uid, ids, context=None):
2762 #TODO try to delete location and route and if not possible, put them in inactive
2763 return super(stock_warehouse, self).unlink(cr, uid, ids, context=context)
2765 def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
2767 all_routes += [warehouse.crossdock_route_id.id]
2768 all_routes += [warehouse.reception_route_id.id]
2769 all_routes += [warehouse.delivery_route_id.id]
2770 all_routes += [warehouse.mto_pull_id.route_id.id]
2771 all_routes += [route.id for route in warehouse.resupply_route_ids]
2772 all_routes += [route.id for route in warehouse.route_ids]
2775 def view_all_routes_for_wh(self, cr, uid, ids, context=None):
2777 for wh in self.browse(cr, uid, ids, context=context):
2778 all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
2780 domain = [('id', 'in', all_routes)]
2782 'name': _('Warehouse\'s Routes'),
2784 'res_model': 'stock.location.route',
2785 'type': 'ir.actions.act_window',
2787 'view_mode': 'tree,form',
2788 'view_type': 'form',
2792 class stock_location_path(osv.osv):
2793 _name = "stock.location.path"
2794 _description = "Pushed Flows"
2797 def _get_route(self, cr, uid, ids, context=None):
2798 #WARNING TODO route_id is not required, so a field related seems a bad idea >-<
2804 context_with_inactive = context.copy()
2805 context_with_inactive['active_test']=False
2806 for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive):
2807 for push_rule in route.push_ids:
2808 result[push_rule.id] = True
2809 return result.keys()
2812 'name': fields.char('Operation Name', size=64, required=True),
2813 'company_id': fields.many2one('res.company', 'Company'),
2814 'route_id': fields.many2one('stock.location.route', 'Route'),
2815 'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
2816 'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
2817 'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
2818 'invoice_state': fields.selection([
2819 ("invoiced", "Invoiced"),
2820 ("2binvoiced", "To Be Invoiced"),
2821 ("none", "Not Applicable")], "Invoice Status",
2823 '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"),
2824 'auto': fields.selection(
2825 [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
2827 required=True, select=1,
2828 help="This is used to define paths the product has to follow within the location tree.\n" \
2829 "The 'Automatic Move' value will create a stock move after the current one that will be "\
2830 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
2831 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
2833 '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'),
2834 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={
2835 'stock.location.route': (_get_route, ['active'], 20),
2836 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),},
2837 help="If the active field is set to False, it will allow you to hide the rule without removing it." ),
2838 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
2843 'invoice_state': 'none',
2844 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
2848 def _apply(self, cr, uid, rule, move, context=None):
2849 move_obj = self.pool.get('stock.move')
2850 newdate = (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta.relativedelta(days=rule.delay or 0)).strftime('%Y-%m-%d')
2851 if rule.auto=='transparent':
2852 move_obj.write(cr, uid, [move.id], {
2854 'location_dest_id': rule.location_dest_id.id
2856 if rule.location_dest_id.id<>move.location_dest_id.id:
2857 move_obj._push_apply(self, cr, uid, move.id, context)
2860 move_id = move_obj.copy(cr, uid, move.id, {
2861 'location_id': move.location_dest_id.id,
2862 'location_dest_id': rule.location_dest_id.id,
2863 'date': datetime.now().strftime('%Y-%m-%d'),
2864 'company_id': rule.company_id and rule.company_id.id or False,
2865 'date_expected': newdate,
2866 'picking_id': False,
2867 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
2869 'propagate': rule.propagate,
2870 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
2872 move_obj.write(cr, uid, [move.id], {
2873 'move_dest_id': move_id,
2875 move_obj.action_confirm(cr, uid, [move_id], context=None)
2878 class stock_move_putaway(osv.osv):
2879 _name = 'stock.move.putaway'
2880 _description = 'Proposed Destination'
2882 'move_id': fields.many2one('stock.move', required=True),
2883 'location_id': fields.many2one('stock.location', 'Location', required=True),
2884 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
2885 'quantity': fields.float('Quantity', required=True),
2890 # -------------------------
2891 # Packaging related stuff
2892 # -------------------------
2894 from openerp.report import report_sxw
2895 report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package', 'addons/stock/report/picking_barcode.rml')
2897 class stock_package(osv.osv):
2899 These are the packages, containing quants and/or other packages
2901 _name = "stock.quant.package"
2902 _description = "Physical Packages"
2903 _parent_name = "parent_id"
2904 _parent_store = True
2905 _parent_order = 'name'
2906 _order = 'parent_left'
2908 def name_get(self, cr, uid, ids, context=None):
2909 res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
2912 def _complete_name(self, cr, uid, ids, name, args, context=None):
2913 """ Forms complete name of location from parent location to child location.
2914 @return: Dictionary of values
2917 for m in self.browse(cr, uid, ids, context=context):
2919 parent = m.parent_id
2921 res[m.id] = parent.name + ' / ' + res[m.id]
2922 parent = parent.parent_id
2925 def _get_packages(self, cr, uid, ids, context=None):
2926 """Returns packages from quants for store"""
2928 for quant in self.browse(cr, uid, ids, context=context):
2929 if quant.package_id:
2930 res.add(quant.package_id.id)
2933 def _get_packages_to_relocate(self, cr, uid, ids, context=None):
2935 for pack in self.browse(cr, uid, ids, context=context):
2938 res.add(pack.parent_id.id)
2941 # TODO: Problem when package is empty!
2943 def _get_package_info(self, cr, uid, ids, name, args, context=None):
2944 default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
2945 res = {}.fromkeys(ids, {'location_id': False, 'company_id': default_company_id})
2946 for pack in self.browse(cr, uid, ids, context=context):
2948 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
2949 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
2950 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
2951 elif pack.children_ids:
2952 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
2953 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
2954 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
2958 'name': fields.char('Package Reference', size=64, select=True),
2959 'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
2960 'parent_left': fields.integer('Left Parent', select=1),
2961 'parent_right': fields.integer('Right Parent', select=1),
2962 'packaging_id': fields.many2one('product.packaging', 'Type of Packaging'),
2963 'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
2965 'stock.quant': (_get_packages, ['location_id'], 10),
2966 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
2968 'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content'),
2969 'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict'),
2970 'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages'),
2971 'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package",
2973 'stock.quant': (_get_packages, ['company_id'], 10),
2974 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
2976 'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
2978 'stock.quant': (_get_packages, ['owner_id'], 10),
2979 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
2983 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
2985 def _check_location(self, cr, uid, ids, context=None):
2986 '''checks that all quants in a package are stored in the same location'''
2987 quant_obj = self.pool.get('stock.quant')
2988 for pack in self.browse(cr, uid, ids, context=context):
2990 while parent.parent_id:
2991 parent = parent.parent_id
2992 quant_ids = self.get_content(cr, uid, [parent.id], context=context)
2993 quants = quant_obj.browse(cr, uid, quant_ids, context=context)
2994 location_id = quants and quants[0].location_id.id or False
2995 if not all([quant.location_id.id == location_id for quant in quants]):
3000 (_check_location, 'Everything inside a package should be in the same location', ['location_id']),
3003 def action_print(self, cr, uid, ids, context=None):
3007 'ids': context.get('active_id') and [context.get('active_id')] or ids,
3008 'model': 'stock.quant.package',
3009 'form': self.read(cr, uid, ids)[0]
3012 'type': 'ir.actions.report.xml',
3013 'report_name': 'stock.quant.package.barcode',
3017 def unpack(self, cr, uid, ids, context=None):
3018 quant_obj = self.pool.get('stock.quant')
3019 for package in self.browse(cr, uid, ids, context=context):
3020 quant_ids = [quant.id for quant in package.quant_ids]
3021 quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3022 children_package_ids = [child_package.id for child_package in package.children_ids]
3023 self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3024 #delete current package since it contains nothing anymore
3025 self.unlink(cr, uid, ids, context=context)
3026 return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3028 def get_content(self, cr, uid, ids, context=None):
3029 child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3030 return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3032 def get_content_package(self, cr, uid, ids, context=None):
3033 quants_ids = self.get_content(cr, uid, ids, context=context)
3034 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3035 res['domain'] = [('id', 'in', quants_ids)]
3038 def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3039 ''' find the total of given product 'product_id' inside the given package 'package_id'''
3040 quant_obj = self.pool.get('stock.quant')
3041 all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3043 for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3044 if quant.product_id.id == product_id:
3049 class stock_pack_operation(osv.osv):
3050 _name = "stock.pack.operation"
3051 _description = "Packing Operation"
3053 def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3055 for ops in self.browse(cr, uid, ids, context=context):
3056 qty = ops.product_qty
3057 for quant in ops.reserved_quant_ids:
3063 'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3064 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
3065 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3066 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3067 'package_id': fields.many2one('stock.quant.package', 'Package'), # 2
3068 'quant_id': fields.many2one('stock.quant', 'Quant'), # 3
3069 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3070 'result_package_id': fields.many2one('stock.quant.package', 'Container Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3071 'date': fields.datetime('Date', required=True),
3072 'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3073 #'update_cost': fields.boolean('Need cost update'),
3074 'cost': fields.float("Cost", help="Unit Cost for this product line"),
3075 'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3076 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_op_id', string='Reserved Quants', readonly=True, help='Quants reserved for this operation'),
3077 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'),
3081 'date': fields.date.context_today,
3085 def _get_domain(self, cr, uid, ops, context=None):
3087 Gives domain for different
3091 res.append(('package_id', '=', ops.package_id.id), )
3093 res.append(('lot_id', '=', ops.lot_id.id), )
3095 res.append(('owner_id', '=', ops.owner_id.id), )
3097 res.append(('owner_id', '=', False), )
3100 #TODO: this function can be refactored
3101 def _search_and_increment(self, cr, uid, picking_id, key, context=None):
3102 '''Search for an operation on an existing key in a picking, if it exists increment the qty (+1) otherwise create it
3104 :param key: tuple directly reusable in a domain
3105 context can receive a key 'current_package_id' with the package to consider for this operation
3108 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
3109 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
3110 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
3111 (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)
3113 quant_obj = self.pool.get('stock.quant')
3117 #if current_package_id is given in the context, we increase the number of items in this package
3118 package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3119 existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id), key] + package_clause, context=context)
3120 if existing_operation_ids:
3121 #existing operation found for the given key and picking => increment its quantity
3122 operation_id = existing_operation_ids[0]
3123 qty = self.browse(cr, uid, operation_id, context=context).product_qty + 1
3124 self.write(cr, uid, operation_id, {'product_qty': qty}, context=context)
3126 #no existing operation found for the given key and picking => create a new one
3127 var_name, dummy, value = key
3129 if var_name == 'product_id':
3130 uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
3131 elif var_name == 'quant_id':
3132 quant = quant_obj.browse(cr, uid, value, context=context)
3133 uom_id = quant.product_id.uom_id.id
3135 'picking_id': picking_id,
3138 'product_uom_id': uom_id,
3140 operation_id = self.create(cr, uid, values, context=context)
3141 values.update({'id': operation_id})
3144 class stock_warehouse_orderpoint(osv.osv):
3146 Defines Minimum stock rules.
3148 _name = "stock.warehouse.orderpoint"
3149 _description = "Minimum Inventory Rule"
3151 def _get_draft_procurements(self, cr, uid, ids, field_name, arg, context=None):
3155 procurement_obj = self.pool.get('procurement.order')
3156 for orderpoint in self.browse(cr, uid, ids, context=context):
3157 procurement_ids = procurement_obj.search(cr, uid, [('state', '=', 'draft'), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)])
3158 result[orderpoint.id] = procurement_ids
3161 def _check_product_uom(self, cr, uid, ids, context=None):
3163 Check if the UoM has the same category as the product standard UoM
3168 for rule in self.browse(cr, uid, ids, context=context):
3169 if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
3175 'name': fields.char('Name', size=32, required=True),
3176 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
3177 'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
3178 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
3179 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
3180 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '!=', 'service')]),
3181 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
3182 'product_min_qty': fields.float('Minimum Quantity', required=True,
3183 help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
3184 "a procurement to bring the forecasted quantity to the Max Quantity."),
3185 'product_max_qty': fields.float('Maximum Quantity', required=True,
3186 help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
3187 "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
3188 'qty_multiple': fields.integer('Qty Multiple', required=True,
3189 help="The procurement quantity will be rounded up to this multiple."),
3190 'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
3191 'company_id': fields.many2one('res.company', 'Company', required=True),
3192 'procurement_draft_ids': fields.function(_get_draft_procurements, type='many2many', relation="procurement.order", \
3193 string="Related Procurement Orders", help="Draft procurement of the product and location of that orderpoint"),
3196 'active': lambda *a: 1,
3197 'logic': lambda *a: 'max',
3198 'qty_multiple': lambda *a: 1,
3199 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3200 'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
3201 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
3203 _sql_constraints = [
3204 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
3207 (_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']),
3210 def default_get(self, cr, uid, fields, context=None):
3211 res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
3212 # default 'warehouse_id' and 'location_id'
3213 if 'warehouse_id' not in res:
3214 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0', context)
3215 res['warehouse_id'] = warehouse.id
3216 if 'location_id' not in res:
3217 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, res['warehouse_id'], context)
3218 res['location_id'] = warehouse.lot_stock_id.id
3221 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
3222 """ Finds location id for changed warehouse.
3223 @param warehouse_id: Changed id of warehouse.
3224 @return: Dictionary of values.
3227 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
3228 v = {'location_id': w.lot_stock_id.id}
3232 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
3233 """ Finds UoM for changed product.
3234 @param product_id: Changed id of product.
3235 @return: Dictionary of values.
3238 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3239 d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
3240 v = {'product_uom': prod.uom_id.id}
3241 return {'value': v, 'domain': d}
3242 return {'domain': {'product_uom': []}}
3244 def copy(self, cr, uid, id, default=None, context=None):
3248 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3250 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
3254 class stock_picking_type(osv.osv):
3255 _name = "stock.picking.type"
3256 _description = "The picking type determines the picking view"
3258 def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
3259 """ Generic method to generate data for bar chart values using SparklineBarWidget.
3260 This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
3262 :param obj: the target model (i.e. crm_lead)
3263 :param domain: the domain applied to the read_group
3264 :param list read_fields: the list of fields to read in the read_group
3265 :param str value_field: the field used to compute the value of the bar slice
3266 :param str groupby_field: the fields used to group
3268 :return list section_result: a list of dicts: [
3269 { 'value': (int) bar_column_value,
3270 'tootip': (str) bar_column_tooltip,
3274 month_begin = date.today().replace(day=1)
3277 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
3278 } for i in range(10, -1, -1)]
3279 group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
3280 for group in group_obj:
3281 group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
3282 month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
3283 section_result[10 - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
3284 return section_result
3286 def _get_picking_data(self, cr, uid, ids, field_name, arg, context=None):
3287 obj = self.pool.get('stock.picking')
3288 res = dict.fromkeys(ids, False)
3289 month_begin = date.today().replace(day=1)
3290 groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3291 groupby_end = (month_begin + relativedelta.relativedelta(months=3)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3294 ('picking_type_id', '=', id),
3295 ('state', 'not in', ['done', 'cancel']),
3296 ('date', '>=', groupby_begin),
3297 ('date', '<', groupby_end),
3299 res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['date'], 'picking_type_id_count', 'date', context=context)
3302 def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
3303 obj = self.pool.get('stock.picking')
3305 'count_picking_draft': [('state', '=', 'draft')],
3306 'count_picking_waiting': [('state','=','confirmed')],
3307 'count_picking_ready': [('state','=','assigned')],
3308 'count_picking': [('state','in',('assigned','waiting','confirmed'))],
3309 'count_picking_late': [('min_date','<', time.strftime('%Y-%m-%d %H:%M:%S')), ('state','in',('assigned','waiting','confirmed'))],
3310 'count_picking_backorders': [('backorder_id','<>', False), ('state','!=','done')],
3313 for field in domains:
3314 data = obj.read_group(cr, uid, domains[field] +
3315 [('state', 'not in',('done','cancel')), ('picking_type_id', 'in', ids)],
3316 ['picking_type_id'], ['picking_type_id'], context=context)
3317 count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
3319 result.setdefault(tid, {})[field] = count.get(tid, 0)
3321 if result[tid]['count_picking']:
3322 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] *100 / result[tid]['count_picking']
3323 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] *100 / (result[tid]['count_picking'] + result[tid]['count_picking_draft'])
3325 result[tid]['rate_picking_late'] = 0
3326 result[tid]['rate_picking_backorders'] = 0
3329 #TODO: not returning valus in required format to show in sparkline library,just added latest_picking_waiting need to add proper logic.
3330 def _get_picking_history(self, cr, uid, ids, field_names, arg, context=None):
3331 obj = self.pool.get('stock.picking')
3335 'latest_picking_late': [],
3336 'latest_picking_backorders': [],
3337 'latest_picking_waiting': []
3340 pick_ids = obj.search(cr, uid, [('state', '=','done'), ('picking_type_id','=',type_id)], limit=12, order="date desc", context=context)
3341 for pick in obj.browse(cr, uid, pick_ids, context=context):
3342 result[type_id]['latest_picking_late'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3343 result[type_id]['latest_picking_backorders'] = bool(pick.backorder_id)
3344 result[type_id]['latest_picking_waiting'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3347 def onchange_picking_code(self, cr, uid, ids, picking_code=False):
3348 if not picking_code:
3351 obj_data = self.pool.get('ir.model.data')
3352 stock_loc = obj_data.get_object_reference(cr, uid, 'stock','stock_location_stock')[1]
3355 'default_location_src_id': stock_loc,
3356 'default_location_dest_id': stock_loc,
3358 if picking_code == 'incoming':
3359 result['default_location_src_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_suppliers')[1]
3360 return {'value': result}
3361 if picking_code == 'outgoing':
3362 result['default_location_dest_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_customers')[1]
3363 return {'value': result}
3365 return {'value': result}
3367 def _get_name(self, cr, uid, ids, field_names, arg, context=None):
3368 return dict(self.name_get(cr, uid, ids, context=context))
3370 def name_get(self, cr, uid, ids, context=None):
3371 """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
3374 if not isinstance(ids, list):
3379 for record in self.browse(cr, uid, ids, context=context):
3381 if record.warehouse_id:
3382 name = record.warehouse_id.name + ': ' +name
3383 if context.get('special_shortened_wh_name'):
3384 if record.warehouse_id:
3385 name = record.warehouse_id.name
3387 name = _('Customer') + ' (' + record.name + ')'
3388 res.append((record.id, name))
3391 def _default_warehouse(self, cr, uid, context=None):
3392 user = self.pool.get('res.users').browse(cr, uid, uid, context)
3393 res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
3394 return res and res[0] or False
3397 'name': fields.char('name', translate=True, required=True),
3398 'complete_name': fields.function(_get_name, type='char', string='Name'),
3399 'pack': fields.boolean('Prefill Pack Operations', help='This picking type needs packing interface'),
3400 'auto_force_assign': fields.boolean('Automatic Availability', help='This picking type does\'t need to check for the availability in source location.'),
3401 'color': fields.integer('Color Index'),
3402 'delivery': fields.boolean('Print delivery'),
3403 'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
3404 'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
3405 'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
3406 #TODO: change field name to "code" as it's not a many2one anymore
3407 'code_id': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Picking type code', required=True),
3408 'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
3409 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
3410 'active': fields.boolean('Active'),
3412 # Statistics for the kanban view
3413 'weekly_picking': fields.function(_get_picking_data,
3415 string='Scheduled pickings per week'),
3417 'count_picking_draft': fields.function(_get_picking_count,
3418 type='integer', multi='_get_picking_count'),
3419 'count_picking_ready': fields.function(_get_picking_count,
3420 type='integer', multi='_get_picking_count'),
3421 'count_picking': fields.function(_get_picking_count,
3422 type='integer', multi='_get_picking_count'),
3423 'count_picking_waiting': fields.function(_get_picking_count,
3424 type='integer', multi='_get_picking_count'),
3425 'count_picking_late': fields.function(_get_picking_count,
3426 type='integer', multi='_get_picking_count'),
3427 'count_picking_backorders': fields.function(_get_picking_count,
3428 type='integer', multi='_get_picking_count'),
3430 'rate_picking_late': fields.function(_get_picking_count,
3431 type='integer', multi='_get_picking_count'),
3432 'rate_picking_backorders': fields.function(_get_picking_count,
3433 type='integer', multi='_get_picking_count'),
3435 'latest_picking_late': fields.function(_get_picking_history,
3436 type='string', multi='_get_picking_history'),
3437 'latest_picking_backorders': fields.function(_get_picking_history,
3438 type='string', multi='_get_picking_history'),
3439 'latest_picking_waiting': fields.function(_get_picking_history,
3440 type='string', multi='_get_picking_history'),
3444 'warehouse_id': _default_warehouse,
3449 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: