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('Applicable on Product'),
176 'product_categ_selectable': fields.boolean('Applicable on Product Category'),
177 'warehouse_selectable': fields.boolean('Applicable 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 action_view_quant_history(self, cr, uid, ids, context=None):
244 This function returns an action that display the history of the quant, which
245 mean all the stock moves that lead to this quant creation with this quant quantity.
247 mod_obj = self.pool.get('ir.model.data')
248 act_obj = self.pool.get('ir.actions.act_window')
250 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_move_form2')
251 id = result and result[1] or False
252 result = act_obj.read(cr, uid, [id], context={})[0]
255 for quant in self.browse(cr, uid, ids, context=context):
256 move_ids += [move.id for move in quant.history_ids]
258 result['domain'] = "[('id','in',[" + ','.join(map(str, move_ids)) + "])]"
261 def quants_reserve(self, cr, uid, quants, move, context=None):
263 for quant,qty in quants:
264 if not quant: continue
265 self._quant_split(cr, uid, quant, qty, context=context)
266 toreserve.append(quant.id)
267 return self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id}, context=context)
269 # add location_dest_id in parameters (False=use the destination of the move)
270 def quants_move(self, cr, uid, quants, move, lot_id = False, owner_id = False, package_id = False, context=None):
271 for quant, qty in quants:
272 #quant may be a browse record or None
273 quant_record = self.move_single_quant(cr, uid, quant, qty, move, lot_id = lot_id, package_id = package_id, context=context)
274 #quant_record is the quant newly created or already split
275 self._quant_reconcile_negative(cr, uid, quant_record, context=context)
278 def check_preferred_location(self, cr, uid, move, context=None):
279 if move.putaway_ids and move.putaway_ids[0]:
280 #Take only first suggestion for the moment
281 return move.putaway_ids[0].location_id
282 return move.location_dest_id
284 def move_single_quant(self, cr, uid, quant, qty, move, lot_id = False, owner_id = False, package_id = False, context=None):
286 quant = self._quant_create(cr, uid, qty, move, lot_id = lot_id, owner_id = owner_id, package_id = package_id, context = context)
288 self._quant_split(cr, uid, quant, qty, context=context)
289 # FP Note: improve this using preferred locations
290 location_to = self.check_preferred_location(cr, uid, move, context=context)
291 self.write(cr, SUPERUSER_ID, [quant.id], {
292 'location_id': location_to.id,
293 'history_ids': [(4, move.id)]
298 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):
299 ''' This function tries to find quants in the given location for the given domain, by trying to first limit
300 the choice on the quants that match the prefered_domain as well. But if the qty requested is not reached
301 it tries to find the remaining quantity by using the fallback_domain.
303 if prefered_domain and fallback_domain:
306 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)
311 quant_ids.append(quant[0].id)
314 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
316 #make sure the quants aren't found twice (if the prefered_domain and the fallback_domain aren't orthogonal
317 domain += [('id', 'not in', quant_ids)]
318 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)
319 for quant in unprefered_quants:
322 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)
324 def quants_get(self, cr, uid, location, product, qty, domain=None, restrict_lot_id=False, restrict_partner_id=False, context=None):
326 Use the removal strategies of product to search for the correct quants
327 If you inherit, put the super at the end of your method.
329 :location: browse record of the parent location in which the quants have to be found
330 :product: browse record of the product to find
331 :qty in UoM of product
334 domain = domain or [('qty', '>', 0.0)]
335 if restrict_partner_id:
336 domain += [('owner_id', '=', restrict_partner_id)]
338 domain += [('lot_id', '=', restrict_lot_id)]
340 removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) or 'fifo'
341 if removal_strategy == 'fifo':
342 result += self._quants_get_fifo(cr, uid, location, product, qty, domain, context=context)
343 elif removal_strategy == 'lifo':
344 result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context)
346 raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,)))
349 def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, package_id = False, force_location=False, context=None):
350 '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location.
354 price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
355 location = force_location or move.location_dest_id
357 'product_id': move.product_id.id,
358 'location_id': location.id,
361 'history_ids': [(4, move.id)],
362 'in_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
363 'company_id': move.company_id.id,
365 'owner_id': owner_id,
368 if move.location_id.usage == 'internal':
369 #if we were trying to move something from an internal location and reach here (quant creation),
370 #it means that a negative quant has to be created as well.
371 negative_vals = vals.copy()
372 negative_vals['location_id'] = move.location_id.id
373 negative_vals['qty'] = -qty
374 negative_vals['cost'] = price_unit
375 negative_vals['negative_dest_location_id'] = move.location_dest_id.id
376 negative_vals['package_id'] = package_id
377 negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
378 vals.update({'propagated_from_id': negative_quant_id})
380 #create the quant as superuser, because we want to restrict the creation of quant manually: they should always use this method to create quants
381 quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
382 return self.browse(cr, uid, quant_id, context=context)
384 def _quant_split(self, cr, uid, quant, qty, context=None):
385 context = context or {}
386 if (quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty):
388 new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty}, context=context)
389 self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context)
391 return self.browse(cr, uid, new_quant, context=context)
393 def _get_latest_move(self, cr, uid, quant, context=None):
395 for m in quant.history_ids:
396 if not move or m.date > move.date:
400 #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
401 # move = self._get_latest_move(cr, uid, to_solve_quant, context=context)
402 # remaining_solving_quant = self._quant_split(cr, uid, quant, qty, context=context)
403 # remaining_to_solve_quant = self._quant_split(cr, uid, to_solve_quant, qty, context=context)
404 # remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
405 # #if the reconciliation was not complete, we need to link together the remaining parts
406 # if remaining_to_solve_quant and remaining_neg_quant:
407 # self.write(cr, uid, remaining_to_solve_quant.id, {'propagated_from_id': remaining_neg_quant.id}, context=context)
408 # #delete the reconciled quants, as it is replaced by the solving quant
409 # if remaining_neg_quant:
410 # otherquant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
411 # self.write(cr, uid, otherquant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
412 # self.unlink(cr, SUPERUSER_ID, [quant_neg.id, to_solve_quant.id], context=context)
413 # #call move_single_quant to ensure recursivity if necessary and do the stock valuation
414 # self.move_single_quant(cr, uid, quant, qty, move, context=context)
415 # return remaining_solving_quant, remaining_to_solve_quant
417 def _quants_merge(self, cr, uid, solved_quant_ids, solving_quant, context=None):
419 for move in solving_quant.history_ids:
420 path.append((4, move.id))
421 self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
423 def _quant_reconcile_negative(self, cr, uid, quant, context=None):
425 When new quant arrive in a location, try to reconcile it with
426 negative quants. If it's possible, apply the cost of the new
427 quant to the conter-part of the negative quant.
429 if quant.location_id.usage != 'internal':
431 solving_quant = quant
432 dom = [('qty', '<', 0)]
433 dom += [('lot_id', '=', quant.lot_id and quant.lot_id.id or False)]
434 dom += [('owner_id', '=', quant.owner_id and quant.owner_id.id or False)]
435 dom += [('package_id', '=', quant.package_id and quant.package_id.id or False)]
436 quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, [('qty', '<', '0')], context=context)
437 for quant_neg, qty in quants:
440 to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
441 if not to_solve_quant_ids:
444 solved_quant_ids = []
445 for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
448 solved_quant_ids.append(to_solve_quant.id)
449 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
450 solving_qty -= min(solving_qty, to_solve_quant.qty)
451 remaining_solving_quant = self._quant_split(cr, uid, solving_quant, qty, context=context)
452 remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
453 #if the reconciliation was not complete, we need to link together the remaining parts
454 if remaining_neg_quant:
455 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
456 if remaining_to_solve_quant_ids:
457 self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
458 #delete the reconciled quants, as it is replaced by the solved quants
459 self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
460 #price update + accounting entries adjustments
461 self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
462 #merge history (and cost?)
463 self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
464 self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
465 solving_quant = remaining_solving_quant
467 #solving_quant, dummy = self._reconcile_single_negative_quant(cr, uid, to_solve_quant, solving_quant, quant_neg, qty, context=context)
469 def _price_update(self, cr, uid, ids, newprice, context=None):
470 self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context)
472 def write(self, cr, uid, ids, vals, context=None):
473 #We want to trigger the move with nothing on reserved_quant_ids for the store of the remaining quantity
474 if 'reservation_id' in vals:
475 reservation_ids = self.browse(cr, uid, ids, context=context)
476 moves_to_warn = set()
477 for reser in reservation_ids:
478 if reser.reservation_id:
479 moves_to_warn.add(reser.reservation_id.id)
480 self.pool.get('stock.move').write(cr, uid, list(moves_to_warn), {'reserved_quant_ids': []}, context=context)
481 return super(stock_quant, self).write(cr, SUPERUSER_ID, ids, vals, context=context)
483 def quants_unreserve(self, cr, uid, move, context=None):
484 related_quants = [x.id for x in move.reserved_quant_ids]
485 return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'reservation_op_id': False}, context=context)
487 def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
488 ''' Implementation of removal strategies
489 If it can not reserve, it will return a tuple (None, qty)
491 domain += location and [('location_id', 'child_of', location.id)] or []
492 domain += [('product_id', '=', product.id)] + domain
496 quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
498 res.append((None, quantity))
500 for quant in self.browse(cr, uid, quants, context=context):
501 if quantity >= abs(quant.qty):
502 res += [(quant, abs(quant.qty))]
503 quantity -= abs(quant.qty)
505 res += [(quant, quantity)]
511 def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None):
513 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
515 def _quants_get_lifo(self, cr, uid, location, product, quantity, domain=[], context=None):
516 order = 'in_date desc'
517 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
519 def _location_owner(self, cr, uid, quant, location, context=None):
520 ''' Return the company owning the location if any '''
521 return location and (location.usage == 'internal') and location.company_id or False
523 def _check_location(self, cr, uid, ids, context=None):
524 for record in self.browse(cr, uid, ids, context=context):
525 if record.location_id.usage == 'view':
526 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))
529 # FP Note: rehab this, with the auto creation algo
530 # def _check_tracking(self, cr, uid, ids, context=None):
531 # """ Checks if serial number is assigned to stock move or not.
532 # @return: True or False
534 # for move in self.browse(cr, uid, ids, context=context):
535 # if not move.lot_id and \
536 # (move.state == 'done' and \
538 # (move.product_id.track_production and move.location_id.usage == 'production') or \
539 # (move.product_id.track_production and move.location_dest_id.usage == 'production') or \
540 # (move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
541 # (move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') or \
542 # (move.product_id.track_incoming and move.location_id.usage == 'inventory') \
548 (_check_location, 'You cannot move products to a location of the type view.', ['location_id'])
549 # (_check_tracking, 'You must assign a serial number for this product.', ['prodlot_id']),
553 #----------------------------------------------------------
555 #----------------------------------------------------------
557 class stock_picking(osv.osv):
558 _name = "stock.picking"
559 _inherit = ['mail.thread']
560 _description = "Picking List"
561 _order = "priority desc, date desc, id desc"
563 def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
564 move_obj = self.pool.get("stock.move")
566 move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
567 move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
569 def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
570 """ Finds minimum and maximum dates for picking.
571 @return: Dictionary of values
575 res[id] = {'min_date': False, 'max_date': False}
587 picking_id""",(tuple(ids),))
588 for pick, dt1, dt2 in cr.fetchall():
589 res[pick]['min_date'] = dt1
590 res[pick]['max_date'] = dt2
593 def create(self, cr, user, vals, context=None):
594 context = context or {}
595 if ('name' not in vals) or (vals.get('name') in ('/', False)):
596 ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
597 sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
598 vals['name'] = self.pool.get('ir.sequence').get_id(cr, user, sequence_id, 'id', context=context)
600 return super(stock_picking, self).create(cr, user, vals, context)
602 def _state_get(self, cr, uid, ids, field_name, arg, context=None):
603 '''The state of a picking depends on the state of its related stock.move
604 draft: the picking has no line or any one of the lines is draft
605 done, draft, cancel: all lines are done / draft / cancel
606 confirmed, auto, assigned depends on move_type (all at once or direct)
609 for pick in self.browse(cr, uid, ids, context=context):
610 if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]):
611 res[pick.id] = 'draft'
613 if all([x.state == 'cancel' for x in pick.move_lines]):
614 res[pick.id] = 'cancel'
616 if all([x.state in ('cancel','done') for x in pick.move_lines]):
617 res[pick.id] = 'done'
620 order = {'confirmed':0, 'waiting':1, 'assigned':2}
621 order_inv = dict(zip(order.values(),order.keys()))
622 lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel','done')]
623 if pick.move_lines == 'one':
624 res[pick.id] = order_inv[min(lst)]
626 res[pick.id] = order_inv[max(lst)]
629 def _get_pickings(self, cr, uid, ids, context=None):
631 for move in self.browse(cr, uid, ids, context=context):
633 res.add(move.picking_id.id)
636 def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
638 for pick in self.browse(cr, uid, ids, context=context):
640 if pick.pack_operation_ids:
644 def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
646 for pick in self.browse(cr, uid, ids, context=context):
648 for move in pick.move_lines:
649 if move.reserved_quant_ids:
654 def action_assign_owner(self, cr, uid, ids, context=None):
655 for picking in self.browse(cr, uid, ids, context=context):
656 packop_ids = [op.id for op in picking.pack_operation_ids]
657 self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
660 'name': fields.char('Reference', size=64, select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
661 'origin': fields.char('Source Document', size=64, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="Reference of the document", select=True),
662 '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),
663 'note': fields.text('Notes', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
664 '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"),
665 'state': fields.function(_state_get, type="selection", store = {
666 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'move_lines'], 20),
667 'stock.move': (_get_pickings, ['state', 'picking_id'], 20)}, selection = [
669 ('cancel', 'Cancelled'),
670 ('waiting', 'Waiting Another Operation'),
671 ('confirmed', 'Waiting Availability'),
672 ('assigned', 'Ready to Transfer'),
673 ('done', 'Transferred'),
674 ], string='Status', readonly=True, select=True, track_visibility='onchange', help="""
675 * Draft: not confirmed yet and will not be scheduled until confirmed\n
676 * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
677 * Waiting Availability: still waiting for the availability of products\n
678 * Ready to Transfer: products reserved, simply waiting for confirmation.\n
679 * Transferred: has been processed, can't be modified or cancelled anymore\n
680 * Cancelled: has been cancelled, can't be confirmed anymore"""
682 'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], string='Priority', required=True),
683 'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
684 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"),
685 'max_date': fields.function(get_min_max_date, multi="min_max_date",
686 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"),
687 '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)]}),
688 'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
689 'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
690 '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'),
691 'partner_id': fields.many2one('res.partner', 'Partner', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
692 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
693 'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', string='Related Packing Operations'),
694 'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
695 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', required=True),
697 'owner_id': fields.many2one('res.partner', 'Owner', help="Default Owner"),
698 # Used to search on pickings
699 'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),#?
700 'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
701 'location_dest_id': fields.related('move_lines', 'location_dest_id', type='many2one', relation='stock.location', string='Destination Location', readonly=True),
702 'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True,
704 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
705 'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
710 'name': lambda self, cr, uid, context: '/',
713 'priority' : '1', #normal
714 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
715 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c)
718 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
721 def copy(self, cr, uid, id, default=None, context=None):
724 default = default.copy()
725 picking_obj = self.browse(cr, uid, id, context=context)
726 if ('name' not in default) or (picking_obj.name == '/'):
727 default['name'] = '/'
728 if not default.get('backorder_id'):
729 default['backorder_id'] = False
731 return super(stock_picking, self).copy(cr, uid, id, default, context)
734 def action_confirm(self, cr, uid, ids, context=None):
736 todo_force_assign = []
738 for picking in self.browse(cr, uid, ids, context=context):
739 if picking.picking_type_id.auto_force_assign:
740 todo_force_assign.append(picking.id)
741 for r in picking.move_lines:
742 if r.state == 'draft':
745 self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
747 if todo_force_assign:
748 self.force_assign(cr, uid, todo_force_assign, context=context)
752 def action_assign(self, cr, uid, ids, *args):
753 """ Changes state of picking to available if all moves are confirmed.
756 for pick in self.browse(cr, uid, ids):
757 if pick.state == 'draft':
758 self.action_confirm(cr, uid, [pick.id])
759 move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
761 raise osv.except_osv(_('Warning!'), _('No product available.'))
762 self.pool.get('stock.move').action_assign(cr, uid, move_ids)
765 def force_assign(self, cr, uid, ids, context=None):
766 """ Changes state of picking to available if moves are confirmed or waiting.
769 for pick in self.browse(cr, uid, ids, context=context):
770 move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
771 self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
774 def cancel_assign(self, cr, uid, ids, *args):
775 """ Cancels picking and moves.
778 for pick in self.browse(cr, uid, ids):
779 move_ids = [x.id for x in pick.move_lines]
780 self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
783 def action_cancel(self, cr, uid, ids, context=None):
784 for pick in self.browse(cr, uid, ids, context=context):
785 ids2 = [move.id for move in pick.move_lines]
786 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
789 def action_done(self, cr, uid, ids, context=None):
790 """Changes picking state to done by processing the Stock Moves of the Picking
792 Normally that happens when the button "Done" is pressed on a Picking view.
795 for pick in self.browse(cr, uid, ids, context=context):
797 for move in pick.move_lines:
798 if move.state == 'draft':
799 self.pool.get('stock.move').action_confirm(cr, uid, [move.id],
802 elif move.state in ('assigned','confirmed'):
805 self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
808 def unlink(self, cr, uid, ids, context=None):
809 move_obj = self.pool.get('stock.move')
810 context = context or {}
811 for pick in self.browse(cr, uid, ids, context=context):
812 ids2 = [move.id for move in pick.move_lines]
813 move_obj.action_cancel(cr, uid, ids2, context=context)
814 move_obj.unlink(cr, uid, ids2, context=context)
815 return super(stock_picking, self).unlink(cr, uid, ids, context=context)
817 # Methods for partial pickings
819 def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
821 Move all non-done lines into a new backorder picking
823 if not backorder_moves:
824 backorder_moves = picking.move_lines
825 backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done','cancel')]
826 if 'do_only_split' in context and context['do_only_split']:
827 backorder_move_ids = [x.id for x in backorder_moves if x.id not in context['split']]
829 if backorder_move_ids:
830 backorder_id = self.copy(cr, uid, picking.id, {
833 'pack_operation_ids': [],
834 'backorder_id': picking.id,
836 back_order_name = self.browse(cr, uid, backorder_id, context=context).name
837 self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (back_order_name), context=context)
838 move_obj = self.pool.get("stock.move")
839 move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
841 self.pool.get("stock.picking").action_confirm(cr, uid, [picking.id], context=context)
842 self.action_confirm(cr, uid, [backorder_id], context=context)
846 def do_prepare_partial(self, cr, uid, picking_ids, context=None):
847 context = context or {}
848 pack_operation_obj = self.pool.get('stock.pack.operation')
849 pack_obj = self.pool.get("stock.quant.package")
850 quant_obj = self.pool.get("stock.quant")
851 for picking in self.browse(cr, uid, picking_ids, context=context):
852 for move in picking.move_lines:
853 if move.state != 'assigned': continue
854 #Check which of the reserved quants are entirely in packages (can be in separate method)
855 packages = list(set([x.package_id for x in move.reserved_quant_ids if x.package_id]))
857 for pack in packages:
862 quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
863 if all([x.reservation_id.id == move.id for x in quant_obj.browse(cr, uid, quants, context=context) if x.reservation_id]):
864 good_pack = test_pack.id
865 if test_pack.parent_id:
866 test_pack = test_pack.parent_id
872 done_packages.append(good_pack)
873 done_packages = list(set(done_packages))
875 #Create package operations
876 reserved = set([x.id for x in move.reserved_quant_ids])
877 remaining_qty = move.product_qty
878 for pack in pack_obj.browse(cr, uid, done_packages, context=context):
879 quantl = pack_obj.get_content(cr, uid, [pack.id], context=context)
880 for quant in quant_obj.browse(cr, uid, quantl, context=context):
881 remaining_qty -= quant.qty
882 quants = set(pack_obj.get_content(cr, uid, [pack.id], context=context))
884 pack_operation_obj.create(cr, uid, {
885 'picking_id': picking.id,
886 'package_id': pack.id,
890 yet_to_reserve = list(reserved)
891 #Create operations based on quants
892 for quant in quant_obj.browse(cr, uid, yet_to_reserve, context=context):
893 qty = min(quant.qty, move.product_qty)
895 pack_operation_obj.create(cr, uid, {
896 'picking_id': picking.id,
898 'quant_id': quant.id,
899 'product_id': quant.product_id.id,
900 'lot_id': quant.lot_id and quant.lot_id.id or False,
901 'product_uom_id': quant.product_id.uom_id.id,
902 'owner_id': quant.owner_id and quant.owner_id.id or False,
904 'package_id': quant.package_id and quant.package_id.id or False,
906 if remaining_qty > 0:
907 pack_operation_obj.create(cr, uid, {
908 'picking_id': picking.id,
909 'product_qty': remaining_qty,
910 'product_id': move.product_id.id,
911 'product_uom_id': move.product_id.uom_id.id,
912 'cost': move.product_id.standard_price,
916 def do_rereserve(self, cr, uid, picking_ids, context=None):
918 Needed for parameter create
920 self.rereserve(cr, uid, picking_ids, context=context)
922 def do_unreserve(self,cr,uid,picking_ids, context=None):
924 Will remove all quants for picking in picking_ids
927 quant_obj = self.pool.get("stock.quant")
928 for picking in self.browse(cr, uid, picking_ids, context=context):
929 for move in picking.move_lines:
930 ids_to_free += [quant.id for quant in move.reserved_quant_ids]
932 quant_obj.write(cr, SUPERUSER_ID, ids_to_free, {'reservation_id' : False, 'reservation_op_id': False }, context = context)
934 def _reserve_quants_ops_move(self, cr, uid, ops, move, qty, create=False, context=None):
936 Will return the quantity that could not be reserved
938 quant_obj = self.pool.get("stock.quant")
939 op_obj = self.pool.get("stock.pack.operation")
940 if create and move.location_id.usage != 'internal':
942 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)
943 quant.write({'reservation_op_id': ops.id, 'location_id': move.location_id.id})
944 quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context)
948 dom = op_obj._get_domain(cr, uid, ops, context=context)
949 dom = dom + [('reservation_id', 'not in', [x.id for x in move.picking_id.move_lines])]
950 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)
953 if quant[0]: # If quant can be reserved
955 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
956 quant_obj.write(cr, SUPERUSER_ID, [x[0].id for x in quants if x[0]], {'reservation_op_id': ops.id}, context=context)
959 def rereserve(self, cr, uid, picking_ids, create=False, context=None):
961 This will unreserve all products and reserve the quants from the operations again
962 :return: Tuple (res, res2, resneg)
963 res: dictionary of ops with quantity that could not be processed matching ops and moves
964 res2: dictionary of moves with quantity that could not be processed matching ops and moves
965 resneg: the negative quants to be created: resneg[move][ops] gives negative quant to be created
966 tuple of dictionary with quantities of quant operation and product that can not be matched between ops and moves
967 and dictionary with remaining values on moves
969 quant_obj = self.pool.get("stock.quant")
970 pack_obj = self.pool.get("stock.quant.package")
971 uom_obj = self.pool.get('product.uom')
972 res = {} # Qty still to do from ops
973 res2 = {} #what is left from moves
974 resneg= {} #Number of negative quants to create for move/op
975 for picking in self.browse(cr, uid, picking_ids, context=context):
977 # unreserve everything and initialize res2
978 for move in picking.move_lines:
979 quant_obj.quants_unreserve(cr, uid, move, context=context)
980 res2[move.id] = move.product_qty
982 if move.state == 'assigned':
983 products_moves.setdefault(move.product_id.id, []).append(move)
986 # Resort pack_operation_ids such that package transfers happen first and then the most specific operations from the product
988 orderedpackops = picking.pack_operation_ids
989 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))
991 for ops in orderedpackops:
992 #If a product is specified in the ops, search for appropriate quants
995 move_ids = ops.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[ops.product_id.id]) or []
996 qty_to_do = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, ops.product_qty, to_uom_id=ops.product_id.uom_id.id)
997 while qty_to_do > 0 and move_ids:
998 move = move_ids.pop()
999 if res2[move.id] > qty_to_do:
1004 qty_to_do -= res2[move.id]
1005 neg_qty = self._reserve_quants_ops_move(cr, uid, ops, move, qty, create=create, context=context)
1007 resneg[move.id].setdefault(ops.id, 0)
1008 resneg [move.id][ops.id] += neg_qty
1009 res2[move.id] -= qty
1011 res[ops.id][ops.product_id.id] = qty_to_do
1012 # In case only a package is specified, take all the quants from the package
1013 elif ops.package_id:
1014 quants = quant_obj.browse(cr, uid, pack_obj.get_content(cr, uid, [ops.package_id.id], context=context))
1015 quants = [x for x in quants if x.qty > 0] #Negative quants should not be moved
1016 for quant in quants:
1018 move_ids = quant.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[quant.product_id.id]) or []
1019 qty_to_do = quant.qty
1020 while qty_to_do > 0 and move_ids:
1021 move = move_ids.pop()
1022 if res2[move.id] > qty_to_do:
1027 qty_to_do -= res2[move.id]
1028 quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context)
1029 quant_obj.write(cr, uid, [quant.id], {'reservation_op_id': ops.id}, context=context)
1030 res2[move.id] -= qty
1031 res.setdefault(ops.id, {}).setdefault(quant.product_id.id, 0.0)
1032 res[ops.id][quant.product_id.id] += qty_to_do
1033 return (res, res2, resneg)
1035 def do_partial(self, cr, uid, picking_ids, context=None):
1037 If no pack operation, we do simple action_done of the picking
1038 Otherwise, do the pack operations
1042 stock_move_obj = self.pool.get('stock.move')
1043 for picking in self.browse(cr, uid, picking_ids, context=context):
1044 if not picking.pack_operation_ids:
1045 self.action_done(cr, uid, [picking.id], context=context)
1049 # TODO: quants could have been created already in Supplier, so create parameter could disappear
1050 res = self.rereserve(cr, uid, [picking.id], create = True, context = context) #This time, quants need to be created
1052 orig_moves = picking.move_lines
1054 for orig in orig_moves:
1055 orig_qtys[orig.id] = orig.product_qty
1056 #Add moves that operations need extra
1058 for ops in res[0].keys():
1059 for prod in res[0][ops].keys():
1060 product = self.pool.get('product.product').browse(cr, uid, prod, context=context)
1061 qty = res[0][ops][prod]
1063 #Create moves for products too many on operation
1064 move_id = stock_move_obj.create(cr, uid, {
1065 'name': product.name,
1066 'product_id': product.id,
1067 'product_uom_qty': qty,
1068 'product_uom': product.uom_id.id,
1069 'location_id': picking.location_id.id,
1070 'location_dest_id': picking.location_dest_id.id,
1071 'picking_id': picking.id,
1072 'picking_type_id': picking.picking_type_id.id,
1073 'group_id': picking.group_id.id,
1075 stock_move_obj.action_confirm(cr, uid, [move_id], context=context)
1076 move = stock_move_obj.browse(cr, uid, move_id, context=context)
1077 ops_rec = self.pool.get("stock.pack.operation").browse(cr, uid, ops, context=context)
1078 resneg[move_id] = {}
1079 resneg[move_id][ops] = self._reserve_quants_ops_move(cr, uid, ops_rec, move, qty, create=True, context=context)
1080 extra_moves.append(move_id)
1083 for move in res2.keys():
1085 mov = stock_move_obj.browse(cr, uid, move, context=context)
1086 new_move = stock_move_obj.split(cr, uid, mov, res2[move], context=context)
1087 #Assign move as it was assigned before
1088 stock_move_obj.action_assign(cr, uid, [new_move])
1090 orig_moves = [x for x in orig_moves if res[1][x.id] < orig_qtys[x.id]]
1091 for move in orig_moves + stock_move_obj.browse(cr, uid, extra_moves, context=context):
1092 if move.state == 'draft':
1093 self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context)
1094 todo.append(move.id)
1095 elif move.state in ('assigned','confirmed'):
1096 todo.append(move.id)
1097 if len(todo) and not ('do_only_split' in context and context['do_only_split']):
1098 self.pool.get('stock.move').action_done(cr, uid, todo, negatives = resneg, context=context)
1099 elif 'do_only_split' in context and context['do_only_split']:
1100 context.update({'split': [x.id for x in orig_moves] + extra_moves})
1102 self._create_backorder(cr, uid, picking, context=context)
1105 def do_split(self, cr, uid, picking_ids, context=None):
1107 just split the picking without making it 'done'
1111 ctx = context.copy()
1112 ctx['do_only_split'] = True
1113 self.do_partial(cr, uid, picking_ids, context=ctx)
1116 # Methods for the barcode UI
1118 def get_picking_for_packing_ui(self, cr, uid, context=None):
1119 return self.search(cr, uid, [('state', 'in', ('confirmed', 'assigned')), ('picking_type_id', '=', context.get('default_picking_type_id'))], context=context)
1121 def action_done_from_packing_ui(self, cr, uid, picking_id, only_split_lines=False, context=None):
1122 self.do_partial(cr, uid, picking_id, only_split_lines, context=context)
1123 #return id of next picking to work on
1124 return self.get_picking_for_packing_ui(cr, uid, context=context)
1126 def action_pack(self, cr, uid, picking_ids, context=None):
1127 stock_operation_obj = self.pool.get('stock.pack.operation')
1128 package_obj = self.pool.get('stock.quant.package')
1129 for picking_id in picking_ids:
1130 operation_ids = stock_operation_obj.search(cr, uid, [('picking_id', '=', picking_id), ('result_package_id', '=', False)], context=context)
1132 package_id = package_obj.create(cr, uid, {}, context=context)
1133 stock_operation_obj.write(cr, uid, operation_ids, {'result_package_id': package_id}, context=context)
1136 def _deal_with_quants(self, cr, uid, picking_id, quant_ids, context=None):
1137 stock_operation_obj = self.pool.get('stock.pack.operation')
1139 todo_on_operations = []
1140 for quant in self.pool.get('stock.quant').browse(cr, uid, quant_ids, context=context):
1141 tmp_moves, tmp_operations = stock_operation_obj._search_and_increment(cr, uid, picking_id, ('quant_id', '=', quant.id), context=context)
1142 todo_on_moves += tmp_moves
1143 todo_on_operations += tmp_operations
1144 return todo_on_moves, todo_on_operations
1146 def get_barcode_and_return_todo_stuff(self, cr, uid, picking_id, barcode_str, context=None):
1147 '''This function is called each time there barcode scanner reads an input'''
1148 #TODO: better error messages handling => why not real raised errors
1149 quant_obj = self.pool.get('stock.quant')
1150 package_obj = self.pool.get('stock.quant.package')
1151 product_obj = self.pool.get('product.product')
1152 stock_operation_obj = self.pool.get('stock.pack.operation')
1155 todo_on_operations = []
1156 #check if the barcode correspond to a product
1157 matching_product_ids = product_obj.search(cr, uid, [('ean13', '=', barcode_str)], context=context)
1158 if matching_product_ids:
1159 todo_on_moves, todo_on_operations = stock_operation_obj._search_and_increment(cr, uid, picking_id, ('product_id', '=', matching_product_ids[0]), context=context)
1161 #check if the barcode correspond to a quant
1162 matching_quant_ids = quant_obj.search(cr, uid, [('name', '=', barcode_str)], context=context) # TODO need the location clause
1163 if matching_quant_ids:
1164 todo_on_moves, todo_on_operations = self._deal_with_quants(cr, uid, picking_id, [matching_quant_ids[0]], context=context)
1166 #check if the barcode correspond to a package
1167 matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1168 if matching_package_ids:
1169 included_package_ids = package_obj.search(cr, uid, [('parent_id', 'child_of', matching_package_ids[0])], context=context)
1170 included_quant_ids = quant_obj.search(cr, uid, [('package_id', 'in', included_package_ids)], context=context)
1171 todo_on_moves, todo_on_operations = self._deal_with_quants(cr, uid, picking_id, included_quant_ids, context=context)
1172 #write remaining qty on stock.move, to ease the treatment server side
1173 for todo in todo_on_moves:
1175 self.pool.get('stock.move').write(cr, uid, todo[1], todo[2], context=context)
1177 self.pool.get('stock.move').create(cr, uid, todo[2], context=context)
1178 return {'warnings': error_msg, 'moves_to_update': todo_on_moves, 'operations_to_update': todo_on_operations}
1181 class stock_production_lot(osv.osv):
1182 _name = 'stock.production.lot'
1183 _inherit = ['mail.thread']
1184 _description = 'Lot/Serial'
1186 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number"),
1187 'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"),
1188 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1189 'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants'),
1190 'create_date': fields.datetime('Creation Date'),
1193 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1194 'product_id': lambda x, y, z, c: c.get('product_id', False),
1196 _sql_constraints = [
1197 ('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'),
1201 # ----------------------------------------------------
1203 # ----------------------------------------------------
1205 class stock_move(osv.osv):
1206 _name = "stock.move"
1207 _description = "Stock Move"
1208 _order = 'date_expected desc, id'
1211 def get_price_unit(self, cr, uid, move, context=None):
1212 """ Returns the unit price to store on the quant """
1213 return move.price_unit or move.product_id.standard_price
1215 def name_get(self, cr, uid, ids, context=None):
1217 for line in self.browse(cr, uid, ids, context=context):
1218 name = line.location_id.name + ' > ' + line.location_dest_id.name
1219 if line.product_id.code:
1220 name = line.product_id.code + ': ' + name
1221 if line.picking_id.origin:
1222 name = line.picking_id.origin + '/ ' + name
1223 res.append((line.id, name))
1226 def _quantity_normalize(self, cr, uid, ids, name, args, context=None):
1227 uom_obj = self.pool.get('product.uom')
1229 for m in self.browse(cr, uid, ids, context=context):
1230 res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False)
1233 def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
1235 for move in self.browse(cr, uid, ids, context=context):
1236 res[move.id] = move.product_qty
1237 for quant in move.reserved_quant_ids:
1238 res[move.id] -= quant.qty
1241 def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
1242 res = dict.fromkeys(ids, False)
1243 for move in self.browse(cr, uid, ids, context=context):
1244 if move.state == 'done':
1245 res[move.id] = [q.id for q in move.quant_ids]
1247 res[move.id] = [q.id for q in move.reserved_quant_ids]
1250 def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
1251 quant_obj = self.pool.get('stock.quant')
1252 res = dict.fromkeys(ids, False)
1253 for move in self.browse(cr, uid, ids, context=context):
1254 if move.state == 'done':
1255 res[move.id] = move.product_qty
1257 sublocation_ids = self.pool.get('stock.location').search(cr, uid, [('id', 'child_of', [move.location_id.id])], context=context)
1258 quant_ids = quant_obj.search(cr, uid, [('location_id', 'in', sublocation_ids), ('product_id', '=', move.product_id.id), ('reservation_id', '=', False)], context=context)
1260 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1261 availability += quant.qty
1262 res[move.id] = min(move.product_qty, availability)
1265 def _get_move(self, cr, uid, ids, context=None):
1267 for quant in self.browse(cr, uid, ids, context=context):
1268 if quant.reservation_id:
1269 res.add(quant.reservation_id.id)
1273 'name': fields.char('Description', required=True, select=True),
1274 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1275 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1276 '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)]}),
1277 'date_expected': fields.datetime('Scheduled Date', states={'done': [('readonly', True)]},required=True, select=True, help="Scheduled date for the processing of this move"),
1278 'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type','<>','service')],states={'done': [('readonly', True)]}),
1279 # TODO: improve store to add dependency on product UoM
1280 'product_qty': fields.function(_quantity_normalize, type='float', store=True, string='Quantity',
1281 digits_compute=dp.get_precision('Product Unit of Measure'),
1282 help='Quantity in the default UoM of the product'),
1283 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
1284 required=True,states={'done': [('readonly', True)]},
1285 help="This is the quantity of products from an inventory "
1286 "point of view. For moves in the state 'done', this is the "
1287 "quantity of products that were actually moved. For other "
1288 "moves, this is the quantity of product that is planned to "
1289 "be moved. Lowering this quantity does not generate a "
1290 "backorder. Changing this quantity on assigned moves affects "
1291 "the product reservation, and should be done with care."
1293 'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True,states={'done': [('readonly', True)]}),
1294 'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}),
1295 'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1297 'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1299 '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."),
1300 '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."),
1302 # FP Note: should we remove this?
1303 '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"),
1306 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True),
1307 'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
1309 'picking_id': fields.many2one('stock.picking', 'Reference', select=True, states={'done': [('readonly', True)]}),
1310 'picking_priority': fields.related('picking_id','priority', type='selection', selection=[('0','Low'),('1','Normal'),('2','High')], string='Picking Priority'),
1311 'note': fields.text('Notes'),
1312 'state': fields.selection([('draft', 'New'),
1313 ('cancel', 'Cancelled'),
1314 ('waiting', 'Waiting Another Move'),
1315 ('confirmed', 'Waiting Availability'),
1316 ('assigned', 'Available'),
1318 ], 'Status', readonly=True, select=True,
1319 help= "* New: When the stock move is created and not yet confirmed.\n"\
1320 "* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"\
1321 "* 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"\
1322 "* Available: When products are reserved, it is set to \'Available\'.\n"\
1323 "* Done: When the shipment is processed, the state is \'Done\'."),
1325 '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
1327 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1328 'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Order of", select=True),
1329 'origin': fields.char("Source"),
1330 '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."),
1332 # used for colors in tree views:
1333 'scrapped': fields.related('location_dest_id','scrap_location',type='boolean',relation='stock.location',string='Scrapped', readonly=True),
1335 'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Quants'),
1336 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
1337 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity',
1338 digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]},
1339 store = {'stock.move': (lambda self, cr, uid, ids, c={}: ids , ['product_uom_qty', 'product_uom', 'reserved_quant_ids'], 20),
1340 'stock.quant': (_get_move, ['reservation_id'], 10)}),
1341 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1342 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
1343 'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'),
1344 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
1345 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
1346 'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
1347 'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.quant', string='Lots'),
1348 'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move'),
1349 'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
1350 'availability': fields.function(_get_product_availability, type='float', string='Availability'),
1351 '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'"),
1352 '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'"),
1353 'putaway_ids': fields.one2many('stock.move.putaway', 'move_id', 'Put Away Suggestions'),
1354 '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"),
1355 '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)."),
1358 def _default_location_destination(self, cr, uid, context=None):
1359 context = context or {}
1360 if context.get('default_picking_type_id', False):
1361 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1362 return pick_type.default_location_dest_id and pick_type.default_location_dest_id.id or False
1365 def _default_location_source(self, cr, uid, context=None):
1366 context = context or {}
1367 if context.get('default_picking_type_id', False):
1368 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1369 return pick_type.default_location_src_id and pick_type.default_location_src_id.id or False
1372 def _default_destination_address(self, cr, uid, context=None):
1373 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1374 return user.company_id.partner_id.id
1377 'location_id': _default_location_source,
1378 'location_dest_id': _default_location_destination,
1379 'partner_id': _default_destination_address,
1383 'product_uom_qty': 1.0,
1385 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1386 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1387 'date_expected': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1388 'procure_method': 'make_to_stock',
1392 def _check_uom(self, cr, uid, ids, context=None):
1393 for move in self.browse(cr, uid, ids, context=context):
1394 if move.product_id.uom_id.category_id.id != move.product_uom.category_id.id:
1400 'You try to move a product using a UoM that is not compatible with the UoM of the product moved. Please use an UoM in the same UoM category.',
1403 def copy(self, cr, uid, id, default=None, context=None):
1406 default = default.copy()
1407 default['move_orig_ids'] = []
1408 default['quant_ids'] = []
1409 default['reserved_quant_ids'] = []
1410 default['returned_move_ids'] = []
1411 default['origin_returned_move_id'] = False
1412 default['state'] = 'draft'
1413 return super(stock_move, self).copy(cr, uid, id, default, context)
1415 def _prepare_procurement_from_move(self, cr, uid, move, context=None):
1416 origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or "/")
1417 group_id = move.group_id and move.group_id.id or False
1419 if move.rule_id.group_propagation_option == 'fixed' and move.rule_id.group_id:
1420 group_id = move.rule_id.group_id.id
1421 elif move.rule_id.group_propagation_option == 'none':
1424 'name': move.rule_id and move.rule_id.name or "/",
1426 'company_id': move.company_id and move.company_id.id or False,
1427 'date_planned': move.date,
1428 'product_id': move.product_id.id,
1429 'product_qty': move.product_qty,
1430 'product_uom': move.product_uom.id,
1431 'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_qty,
1432 'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
1433 'location_id': move.location_id.id,
1434 'move_dest_id': move.id,
1435 'group_id': group_id,
1436 'route_ids': [(4, x.id) for x in move.route_ids],
1437 'warehouse_id': move.warehouse_id and move.warehouse_id.id or False,
1440 def _push_apply(self, cr, uid, moves, context):
1441 push_obj = self.pool.get("stock.location.path")
1443 if not move.move_dest_id:
1444 routes = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids]
1445 routes = routes or [x.id for x in move.route_ids]
1447 domain = [('route_id', 'in', routes), ('location_from_id', '=', move.location_dest_id.id)]
1448 if move.warehouse_id:
1449 domain += [('warehouse_id', '=', move.warehouse_id.id)]
1450 rules = push_obj.search(cr, uid, domain, context=context)
1452 rule = push_obj.browse(cr, uid, rules[0], context=context)
1453 push_obj._apply(cr, uid, rule, move, context=context)
1456 # Create the stock.move.putaway records
1457 def _putaway_apply(self,cr, uid, ids, context=None):
1458 moveputaway_obj = self.pool.get('stock.move.putaway')
1459 for move in self.browse(cr, uid, ids, context=context):
1460 putaway = self.pool.get('stock.location').get_putaway_strategy(cr, uid, move.location_dest_id, move.product_id, context=context)
1462 # Should call different methods here in later versions
1463 # TODO: take care of lots
1464 if putaway.method == 'fixed' and putaway.location_spec_id:
1465 moveputaway_obj.create(cr, SUPERUSER_ID, {'move_id': move.id,
1466 'location_id': putaway.location_spec_id.id,
1467 'quantity': move.product_uom_qty}, context=context)
1470 def _create_procurement(self, cr, uid, move, context=None):
1472 This will create a procurement order
1474 proc_obj = self.pool.get("procurement.order")
1475 return proc_obj.create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
1477 # Check that we do not modify a stock.move which is done
1478 def write(self, cr, uid, ids, vals, context=None):
1479 if isinstance(ids, (int, long)):
1481 frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1482 for move in self.browse(cr, uid, ids, context=context):
1483 if move.state == 'done':
1484 if frozen_fields.intersection(vals):
1485 raise osv.except_osv(_('Operation Forbidden!'),
1486 _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
1487 result = super(stock_move, self).write(cr, uid, ids, vals, context=context)
1490 def onchange_quantity(self, cr, uid, ids, product_id, product_qty,
1491 product_uom, product_uos):
1492 """ On change of product quantity finds UoM and UoS quantities
1493 @param product_id: Product id
1494 @param product_qty: Changed Quantity of product
1495 @param product_uom: Unit of measure of product
1496 @param product_uos: Unit of sale of product
1497 @return: Dictionary of values
1500 'product_uos_qty': 0.00
1504 if (not product_id) or (product_qty <=0.0):
1505 result['product_qty'] = 0.0
1506 return {'value': result}
1508 product_obj = self.pool.get('product.product')
1509 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1511 # Warn if the quantity was decreased
1513 for move in self.read(cr, uid, ids, ['product_qty']):
1514 if product_qty < move['product_qty']:
1516 'title': _('Information'),
1517 'message': _("By changing this quantity here, you accept the "
1518 "new quantity as complete: OpenERP will not "
1519 "automatically generate a back order.") })
1522 if product_uos and product_uom and (product_uom != product_uos):
1523 result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1525 result['product_uos_qty'] = product_qty
1527 return {'value': result, 'warning': warning}
1529 def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1530 product_uos, product_uom):
1531 """ On change of product quantity finds UoM and UoS quantities
1532 @param product_id: Product id
1533 @param product_uos_qty: Changed UoS Quantity of product
1534 @param product_uom: Unit of measure of product
1535 @param product_uos: Unit of sale of product
1536 @return: Dictionary of values
1539 'product_uom_qty': 0.00
1543 if (not product_id) or (product_uos_qty <=0.0):
1544 result['product_uos_qty'] = 0.0
1545 return {'value': result}
1547 product_obj = self.pool.get('product.product')
1548 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1550 # Warn if the quantity was decreased
1551 for move in self.read(cr, uid, ids, ['product_uos_qty']):
1552 if product_uos_qty < move['product_uos_qty']:
1554 'title': _('Warning: No Back Order'),
1555 'message': _("By changing the quantity here, you accept the "
1556 "new quantity as complete: OpenERP will not "
1557 "automatically generate a Back Order.") })
1560 if product_uos and product_uom and (product_uom != product_uos):
1561 result['product_uom_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1563 result['product_uom_qty'] = product_uos_qty
1564 return {'value': result, 'warning': warning}
1566 def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False,
1567 loc_dest_id=False, partner_id=False):
1568 """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
1569 @param prod_id: Changed Product id
1570 @param loc_id: Source location id
1571 @param loc_dest_id: Destination location id
1572 @param partner_id: Address id of partner
1573 @return: Dictionary of values
1577 user = self.pool.get('res.users').browse(cr, uid, uid)
1578 lang = user and user.lang or False
1580 addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id)
1582 lang = addr_rec and addr_rec.lang or False
1583 ctx = {'lang': lang}
1585 product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1586 uos_id = product.uos_id and product.uos_id.id or False
1588 'product_uom': product.uom_id.id,
1589 'product_uos': uos_id,
1590 'product_uom_qty': 1.00,
1591 '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'],
1594 result['name'] = product.partner_ref
1596 result['location_id'] = loc_id
1598 result['location_dest_id'] = loc_dest_id
1599 return {'value': result}
1601 def _picking_assign(self, cr, uid, move, context=None):
1602 if move.picking_id or not move.picking_type_id:
1604 context = context or {}
1605 pick_obj = self.pool.get("stock.picking")
1607 group = move.group_id and move.group_id.id or False
1608 picks = pick_obj.search(cr, uid, [
1609 ('group_id', '=', group),
1610 ('location_id', '=', move.location_id.id),
1611 ('location_dest_id', '=', move.location_dest_id.id),
1612 ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context)
1617 'origin': move.origin,
1618 'company_id': move.company_id and move.company_id.id or False,
1619 'move_type': move.group_id and move.group_id.move_type or 'one',
1620 'partner_id': move.group_id and move.group_id.partner_id and move.group_id.partner_id.id or False,
1621 'date_done': move.date_expected,
1622 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
1624 pick = pick_obj.create(cr, uid, values, context=context)
1625 move.write({'picking_id': pick})
1628 def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
1629 """ On change of Scheduled Date gives a Move date.
1630 @param date_expected: Scheduled Date
1631 @param date: Move Date
1634 if not date_expected:
1635 date_expected = time.strftime('%Y-%m-%d %H:%M:%S')
1636 return {'value':{'date': date_expected}}
1638 def action_confirm(self, cr, uid, ids, context=None):
1639 """ Confirms stock move or put it in waiting if it's linked to another move.
1640 @return: List of ids.
1646 for move in self.browse(cr, uid, ids, context=context):
1648 for m in move.move_orig_ids:
1649 if m.state not in ('done', 'cancel'):
1651 states[state].append(move.id)
1652 self._picking_assign(cr, uid, move, context=context)
1654 for state, write_ids in states.items():
1656 self.write(cr, uid, write_ids, {'state': state})
1657 if state == 'confirmed':
1658 for move in self.browse(cr, uid, write_ids, context=context):
1659 if move.procure_method == 'make_to_order':
1660 self._create_procurement(cr, uid, move, context=context)
1661 moves = self.browse(cr, uid, ids, context=context)
1662 self._push_apply(cr, uid, moves, context=context)
1665 def force_assign(self, cr, uid, ids, context=None):
1666 """ Changes the state to assigned.
1669 done = self.action_assign(cr, uid, ids, context=context)
1670 self.write(cr, uid, list(set(ids) - set(done)), {'state': 'assigned'})
1674 def cancel_assign(self, cr, uid, ids, context=None):
1675 """ Changes the state to confirmed.
1678 return self.write(cr, uid, ids, {'state': 'confirmed'})
1680 def action_assign(self, cr, uid, ids, context=None):
1681 """ Checks the product type and accordingly writes the state.
1682 @return: No. of moves done
1684 context = context or {}
1685 quant_obj = self.pool.get("stock.quant")
1687 for move in self.browse(cr, uid, ids, context=context):
1688 if move.state not in ('confirmed', 'waiting'):
1690 if move.product_id.type == 'consu':
1691 done.append(move.id)
1694 qty = move.product_qty
1696 for m2 in move.move_orig_ids:
1697 for q in m2.quant_ids:
1698 dp.append(str(q.id))
1700 domain = ['|', ('reservation_id', '=', False), ('reservation_id', '=', move.id), ('qty', '>', 0)]
1701 prefered_domain = dp and [('id', 'not in', dp)] or []
1702 fallback_domain = dp and [('id', 'in', dp)] or []
1703 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)
1704 #Will only reserve physical quants, no negative
1705 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
1706 # the total quantity is provided by existing quants
1707 if all(map(lambda x:x[0], quants)):
1708 done.append(move.id)
1709 self.write(cr, uid, done, {'state': 'assigned'})
1710 self._putaway_apply(cr, uid, ids, context=context)
1715 # Cancel move => cancel others move and pickings
1717 def action_cancel(self, cr, uid, ids, context=None):
1718 """ Cancels the moves and if all moves are cancelled it cancels the picking.
1721 procurement_obj = self.pool.get('procurement.order')
1722 context = context or {}
1723 for move in self.browse(cr, uid, ids, context=context):
1724 if move.state == 'done':
1725 raise osv.except_osv(_('Operation Forbidden!'),
1726 _('You cannot cancel a stock move that has been set to \'Done\'.'))
1727 if move.reserved_quant_ids:
1728 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
1729 if context.get('cancel_procurement'):
1731 procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
1732 procurement_obj.cancel(cr, uid, procurement_ids, context=context)
1733 elif move.move_dest_id:
1734 #cancel chained moves
1736 self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
1737 elif move.move_dest_id.state == 'waiting':
1738 self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'})
1739 return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1741 #def _get_quants_from_pack(self, cr, uid, ids, context=None):
1743 # Suppose for the moment we don't have any packaging
1746 # for move in self.browse(cr, uid, ids, context=context):
1747 # #Split according to pack wizard if necessary
1748 # res[move.id] = [x.id for x in move.reserved_quant_ids]
1751 def action_done(self, cr, uid, ids, negatives = False, context=None):
1752 """ Makes the move done and if all moves are done, it will finish the picking.
1753 If quants are not assigned yet, it should assign them
1754 Putaway strategies should be applied
1757 context = context or {}
1758 quant_obj = self.pool.get("stock.quant")
1759 ops_obj = self.pool.get("stock.pack.operation")
1760 pack_obj = self.pool.get("stock.quant.package")
1761 todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
1763 self.action_confirm(cr, uid, todo, context=context)
1766 procurement_ids = []
1767 for move in self.browse(cr, uid, ids, context=context):
1768 # if negatives and negatives[move.id]:
1769 # for ops in negatives[move.id].keys():
1770 # quants_to_move = [(None, negatives[move.id, x) for x in negatives]
1771 # quant_obj.quants_move(cr, uid, quants_to_move, move, context=context)
1774 pickings.add(move.picking_id.id)
1775 qty = move.product_qty
1777 # for qty, location_id in move_id.prefered_location_ids:
1778 # quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, context=context)
1779 # quant_obj.quants_move(cr, uid, quants, move, location_dest_id, context=context)
1780 # should replace the above 2 lines
1781 dom = [('qty', '>', 0)]
1782 prefered_domain = [('reservation_id', '=', move.id)]
1783 fallback_domain = [('reservation_id', '=', False)]
1784 if move.picking_id and move.picking_id.pack_operation_ids:
1785 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)
1786 quant_obj.quants_move(cr, uid, quants, move, context=context)
1787 if negatives and move.id in negatives:
1788 for negative_op in negatives[move.id].keys():
1789 ops = ops_obj.browse(cr, uid, negative_op, context=context)
1790 negatives[move.id][negative_op] = quant_obj.quants_move(cr, uid, [(None, negatives[move.id][negative_op])], move,
1791 lot_id = ops.lot_id and ops.lot_id.id or False,
1792 owner_id = ops.owner_id and ops.owner_id.id or False,
1793 package_id = ops.package_id and ops.package_id.id or False, context=context)
1795 reserved_ops = list(set([x.reservation_op_id.id for x in move.reserved_quant_ids]))
1796 for ops in ops_obj.browse(cr, uid, reserved_ops, context=context):
1798 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)
1800 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)
1802 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)
1803 #Will move all quants_get and as such create negative quants
1804 quant_obj.quants_move(cr, uid, quants, move, context=context)
1805 quant_obj.quants_unreserve(cr, uid, move, context=context)
1807 #Check moves that were pushed
1808 if move.move_dest_id.state in ('waiting', 'confirmed'):
1809 other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
1810 ('move_dest_id', '=', move.move_dest_id.id)], context=context)
1811 #If no other moves for the move that got pushed:
1812 if not other_upstream_move_ids and move.move_dest_id.state in ('waiting', 'confirmed'):
1813 self.action_assign(cr, uid, [move.move_dest_id.id], context=context)
1814 if move.procurement_id:
1815 procurement_ids.append(move.procurement_id.id)
1816 self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1817 self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
1820 def unlink(self, cr, uid, ids, context=None):
1821 context = context or {}
1822 for move in self.browse(cr, uid, ids, context=context):
1823 if move.state not in ('draft', 'cancel'):
1824 raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
1825 return super(stock_move, self).unlink(cr, uid, ids, context=context)
1827 def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
1828 """ Move the scrap/damaged product into scrap location
1829 @param cr: the database cursor
1830 @param uid: the user id
1831 @param ids: ids of stock move object to be scrapped
1832 @param quantity : specify scrap qty
1833 @param location_id : specify scrap location
1834 @param context: context arguments
1835 @return: Scraped lines
1837 #quantity should in MOVE UOM
1839 raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
1841 for move in self.browse(cr, uid, ids, context=context):
1842 source_location = move.location_id
1843 if move.state == 'done':
1844 source_location = move.location_dest_id
1845 #Previously used to prevent scraping from virtual location but not necessary anymore
1846 #if source_location.usage != 'internal':
1847 #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
1848 #raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
1849 move_qty = move.product_qty
1850 uos_qty = quantity / move_qty * move.product_uos_qty
1852 'location_id': source_location.id,
1853 'product_uom_qty': quantity,
1854 'product_uos_qty': uos_qty,
1855 'state': move.state,
1857 'location_dest_id': location_id,
1858 #TODO lot_id is now on quant and not on move, need to do something for this
1859 #'lot_id': move.lot_id.id,
1861 new_move = self.copy(cr, uid, move.id, default_val)
1864 product_obj = self.pool.get('product.product')
1865 for product in product_obj.browse(cr, uid, [move.product_id.id], context=context):
1867 uom = product.uom_id.name if product.uom_id else ''
1868 message = _("%s %s %s has been <b>moved to</b> scrap.") % (quantity, uom, product.name)
1869 move.picking_id.message_post(body=message)
1871 self.action_done(cr, uid, res, context=context)
1874 def action_consume(self, cr, uid, ids, quantity, location_id=False, context=None):
1875 """ Consumed product with specific quatity from specific source location
1876 @param cr: the database cursor
1877 @param uid: the user id
1878 @param ids: ids of stock move object to be consumed
1879 @param quantity : specify consume quantity
1880 @param location_id : specify source location
1881 @param context: context arguments
1882 @return: Consumed lines
1884 #quantity should in MOVE UOM
1888 raise osv.except_osv(_('Warning!'), _('Please provide proper quantity.'))
1890 for move in self.browse(cr, uid, ids, context=context):
1891 move_qty = move.product_qty
1893 raise osv.except_osv(_('Error!'), _('Cannot consume a move with negative or zero quantity.'))
1894 quantity_rest = move.product_qty
1895 quantity_rest -= quantity
1896 uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
1897 if quantity_rest <= 0:
1900 quantity = move.product_qty
1902 uos_qty = quantity / move_qty * move.product_uos_qty
1903 if quantity_rest > 0:
1905 'product_uom_qty': quantity,
1906 'product_uos_qty': uos_qty,
1907 'state': move.state,
1908 'location_id': location_id or move.location_id.id,
1910 current_move = self.copy(cr, uid, move.id, default_val)
1911 res += [current_move]
1913 update_val['product_uom_qty'] = quantity_rest
1914 update_val['product_uos_qty'] = uos_qty_rest
1915 self.write(cr, uid, [move.id], update_val)
1918 quantity_rest = quantity
1919 uos_qty_rest = uos_qty
1922 'product_uom_qty' : quantity_rest,
1923 'product_uos_qty' : uos_qty_rest,
1924 'location_id': location_id or move.location_id.id,
1926 self.write(cr, uid, [move.id], update_val)
1928 self.action_done(cr, uid, res, context=context)
1933 def split(self, cr, uid, move, qty, context=None):
1935 Splits qty from move move into a new move
1937 if move.product_qty==qty:
1939 if (move.product_qty < qty) or (qty==0):
1942 uom_obj = self.pool.get('product.uom')
1943 context = context or {}
1945 uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id)
1946 uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
1948 if move.state in ('done', 'cancel'):
1949 raise osv.except_osv(_('Error'), _('You cannot split a move done'))
1952 'product_uom_qty': uom_qty,
1953 'product_uos_qty': uos_qty,
1954 'state': move.state,
1955 'move_dest_id': False,
1956 'reserved_quant_ids': []
1958 new_move = self.copy(cr, uid, move.id, defaults)
1960 self.write(cr, uid, [move.id], {
1961 'product_uom_qty': move.product_uom_qty - uom_qty,
1962 'product_uos_qty': move.product_uos_qty - uos_qty,
1963 # 'reserved_quant_ids': [(6,0,[])] SHOULD NOT CHANGE as it has been reserved already
1966 if move.move_dest_id and move.propagate:
1967 new_move_prop = self.split(cr, uid, move.move_dest_id, qty, context=context)
1968 self.write(cr, uid, [new_move], {'move_dest_id': new_move_prop}, context=context)
1970 self.action_confirm(cr, uid, [new_move], context=context)
1973 class stock_inventory(osv.osv):
1974 _name = "stock.inventory"
1975 _description = "Inventory"
1977 def _get_move_ids_exist(self, cr, uid, ids, field_name, arg, context=None):
1979 for inv in self.browse(cr, uid, ids, context=context):
1985 def _get_available_filters(self, cr, uid, context=None):
1987 This function will return the list of filter allowed according to the options checked
1988 in 'Settings\Warehouse'.
1990 :rtype: list of tuple
1992 #default available choices
1993 res_filter = [('none', ' All products of a whole location'), ('product', 'One product only')]
1994 settings_obj = self.pool.get('stock.config.settings')
1995 config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
1996 #If we don't have updated config until now, all fields are by default false and so should be not dipslayed
2000 stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
2001 if stock_settings.group_stock_tracking_owner:
2002 res_filter.append(('owner', 'One owner only'))
2003 res_filter.append(('product_owner', 'One product for a specific owner'))
2004 if stock_settings.group_stock_production_lot:
2005 res_filter.append(('lot', 'One Lot/Serial Number'))
2006 if stock_settings.group_stock_tracking_lot:
2007 res_filter.append(('pack', 'A Pack'))
2011 'name': fields.char('Inventory Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
2012 'date': fields.datetime('Inventory Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Create Date."),
2013 'date_done': fields.datetime('Date done', help="Inventory Validation Date."),
2014 'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines."),
2015 'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves."),
2016 'state': fields.selection([('draft', 'Draft'), ('cancel', 'Cancelled'), ('confirm', 'In Progress'), ('done', 'Validated')], 'Status', readonly=True, select=True),
2017 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
2018 'location_id': fields.many2one('stock.location', 'Location', required=True),
2019 'product_id': fields.many2one('product.product', 'Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
2020 '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."),
2021 'partner_id': fields.many2one('res.partner', 'Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
2022 '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."),
2023 'move_ids_exist': fields.function(_get_move_ids_exist, type='boolean', string=' Stock Move Exists?', help='technical field for attrs in view'),
2024 'filter': fields.selection(_get_available_filters, 'Selection Filter'),
2027 def _default_stock_location(self, cr, uid, context=None):
2029 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2030 return warehouse.lot_stock_id.id
2035 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
2037 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2038 'location_id': _default_stock_location,
2041 def set_checked_qty(self, cr, uid, ids, context=None):
2042 inventory = self.browse(cr, uid, ids[0], context=context)
2043 line_ids = [line.id for line in inventory.line_ids]
2044 self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
2047 def copy(self, cr, uid, id, default=None, context=None):
2050 default = default.copy()
2051 default.update({'move_ids': [], 'date_done': False})
2052 return super(stock_inventory, self).copy(cr, uid, id, default, context=context)
2054 def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2055 """ Creates a stock move from an inventory line
2056 @param inventory_line:
2060 return self.pool.get('stock.move').create(cr, uid, move_vals)
2062 def action_done(self, cr, uid, ids, context=None):
2063 """ Finish the inventory
2068 move_obj = self.pool.get('stock.move')
2069 for inv in self.browse(cr, uid, ids, context=context):
2070 if not inv.move_ids:
2071 self.action_check(cr, uid, [inv.id], context=context)
2073 #the action_done on stock_move has to be done in 2 steps:
2074 #first, we start moving the products from stock to inventory loss
2075 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage == 'internal'], context=context)
2076 #then, we move from inventory loss. This 2 steps process is needed because some moved quant may need to be put again in stock
2077 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage != 'internal'], context=context)
2078 self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
2081 def _create_stock_move(self, cr, uid, inventory, todo_line, context=None):
2082 stock_move_obj = self.pool.get('stock.move')
2083 product_obj = self.pool.get('product.product')
2084 inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id
2086 'name': _('INV:') + (inventory.name or ''),
2087 'product_id': todo_line['product_id'],
2088 'product_uom': todo_line['product_uom_id'],
2089 'date': inventory.date,
2090 'company_id': inventory.company_id.id,
2091 'inventory_id': inventory.id,
2092 'state': 'assigned',
2093 'restrict_lot_id': todo_line.get('prod_lot_id'),
2094 'restrict_partner_id': todo_line.get('partner_id'),
2097 if todo_line['product_qty'] < 0:
2098 #found more than expected
2099 vals['location_id'] = inventory_location_id
2100 vals['location_dest_id'] = todo_line['location_id']
2101 vals['product_uom_qty'] = -todo_line['product_qty']
2103 #found less than expected
2104 vals['location_id'] = todo_line['location_id']
2105 vals['location_dest_id'] = inventory_location_id
2106 vals['product_uom_qty'] = todo_line['product_qty']
2107 return stock_move_obj.create(cr, uid, vals, context=context)
2109 def action_check(self, cr, uid, ids, context=None):
2110 """ Checks the inventory and computes the stock move to do
2113 inventory_line_obj = self.pool.get('stock.inventory.line')
2114 stock_move_obj = self.pool.get('stock.move')
2115 for inventory in self.browse(cr, uid, ids, context=context):
2116 #first remove the existing stock moves linked to this inventory
2117 move_ids = [move.id for move in inventory.move_ids]
2118 stock_move_obj.unlink(cr, uid, move_ids, context=context)
2119 #compute what should be in the inventory lines
2120 theorical_lines = self._get_inventory_lines(cr, uid, inventory, context=context)
2121 for line in inventory.line_ids:
2122 #compare the inventory lines to the theorical ones and store the diff in theorical_lines
2123 inventory_line_obj._resolve_inventory_line(cr, uid, line, theorical_lines, context=context)
2124 #each theorical_lines where product_qty is not 0 is a difference for which we need to create a stock move
2125 for todo_line in theorical_lines:
2126 if todo_line['product_qty'] != 0:
2127 self._create_stock_move(cr, uid, inventory, todo_line, context=context)
2129 def action_cancel_draft(self, cr, uid, ids, context=None):
2130 """ Cancels the stock move and change inventory state to draft.
2133 for inv in self.browse(cr, uid, ids, context=context):
2134 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2135 self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
2138 def action_cancel_inventory(self, cr, uid, ids, context=None):
2140 self.action_cancel_draft(cr, uid, ids, context=context)
2142 def prepare_inventory(self, cr, uid, ids, context=None):
2143 inventory_line_obj = self.pool.get('stock.inventory.line')
2144 for inventory in self.browse(cr, uid, ids, context=context):
2145 #clean the existing inventory lines before redoing an inventory proposal
2146 line_ids = [line.id for line in inventory.line_ids]
2147 inventory_line_obj.unlink(cr, uid, line_ids, context=context)
2148 #compute the inventory lines and create them
2149 vals = self._get_inventory_lines(cr, uid, inventory, context=context)
2150 for product_line in vals:
2151 inventory_line_obj.create(cr, uid, product_line, context=context)
2152 return self.write(cr, uid, ids, {'state': 'confirm'})
2154 def _get_inventory_lines(self, cr, uid, inventory, context=None):
2155 location_obj = self.pool.get('stock.location')
2156 product_obj = self.pool.get('product.product')
2157 location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
2158 domain = ' location_id in %s'
2159 args = (tuple(location_ids),)
2160 if inventory.partner_id:
2161 domain += ' and owner_id = %s'
2162 args += (inventory.partner_id.id,)
2163 if inventory.lot_id:
2164 domain += ' and lot_id = %s'
2165 args += (inventory.lot_id.id,)
2166 if inventory.product_id:
2167 domain += 'and product_id = %s'
2168 args += (inventory.product_id.id,)
2169 if inventory.package_id:
2170 domain += ' and package_id = %s'
2171 args += (inventory.package_id.id,)
2173 SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
2174 FROM stock_quant WHERE''' + domain + '''
2175 GROUP BY product_id, location_id, lot_id, package_id, partner_id
2178 for product_line in cr.dictfetchall():
2179 #replace the None the dictionary by False, because falsy values are tested later on
2180 for key, value in product_line.items():
2182 product_line[key] = False
2183 product_line['inventory_id'] = inventory.id
2184 product_line['th_qty'] = product_line['product_qty']
2185 if product_line['product_id']:
2186 product = product_obj.browse(cr, uid, product_line['product_id'], context=context)
2187 product_line['product_uom_id'] = product.uom_id.id
2188 vals.append(product_line)
2191 class stock_inventory_line(osv.osv):
2192 _name = "stock.inventory.line"
2193 _description = "Inventory Line"
2194 _rec_name = "inventory_id"
2196 'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2197 'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
2198 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2199 'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
2200 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
2201 'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
2202 'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
2203 'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
2204 'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
2205 'th_qty': fields.float('Theoretical Quantity', readonly=True),
2206 'partner_id': fields.many2one('res.partner', 'Owner'),
2213 def _resolve_inventory_line(self, cr, uid, inventory_line, theorical_lines, context=None):
2214 #TODO : package_id management !
2216 uom_obj = self.pool.get('product.uom')
2217 for th_line in theorical_lines:
2218 #We try to match the inventory line with a theorical line with same product, lot, location and owner
2219 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:
2220 uom_reference = inventory_line.product_id.uom_id
2221 real_qty = uom_obj._compute_qty_obj(cr, uid, inventory_line.product_uom_id, inventory_line.product_qty, uom_reference)
2222 th_line['product_qty'] -= real_qty
2225 #if it was still not found, we add it to the theorical lines so that it will create a stock move for it
2228 'inventory_id': inventory_line.inventory_id.id,
2229 'location_id': inventory_line.location_id.id,
2230 'product_id': inventory_line.product_id.id,
2231 'product_uom_id': inventory_line.product_id.uom_id.id,
2232 'product_qty': -inventory_line.product_qty,
2233 'prod_lot_id': inventory_line.prod_lot_id.id,
2234 'partner_id': inventory_line.partner_id.id,
2236 theorical_lines.append(vals)
2238 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):
2239 """ Changes UoM and name if product_id changes.
2240 @param location_id: Location id
2241 @param product: Changed product_id
2242 @param uom: UoM product
2243 @return: Dictionary of changed values
2245 context = context or {}
2247 return {'value': {'product_qty': 0.0, 'product_uom_id': False}}
2248 uom_obj = self.pool.get('product.uom')
2249 ctx = context.copy()
2250 ctx['location'] = location_id
2251 ctx['lot_id'] = lot_id
2252 ctx['owner_id'] = owner_id
2253 ctx['package_id'] = package_id
2254 obj_product = self.pool.get('product.product').browse(cr, uid, product, context=ctx)
2255 th_qty = obj_product.qty_available
2256 if uom and uom != obj_product.uom_id.id:
2257 uom_record = uom_obj.browse(cr, uid, uom, context=context)
2258 th_qty = uom_obj._compute_qty_obj(cr, uid, obj_product.uom_id, th_qty, uom_record)
2259 return {'value': {'th_qty': th_qty, 'product_uom_id': uom or obj_product.uom_id.id}}
2262 #----------------------------------------------------------
2264 #----------------------------------------------------------
2265 class stock_warehouse(osv.osv):
2266 _name = "stock.warehouse"
2267 _description = "Warehouse"
2270 'name': fields.char('Name', size=128, required=True, select=True),
2271 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
2272 'partner_id': fields.many2one('res.partner', 'Address'),
2273 'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]),
2274 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage', '=', 'internal')]),
2275 'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"),
2276 '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'),
2277 'reception_steps': fields.selection([
2278 ('one_step', 'Receive goods directly in stock (1 step)'),
2279 ('two_steps', 'Unload in input location then go to stock (2 steps)'),
2280 ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments', required=True),
2281 'delivery_steps': fields.selection([
2282 ('ship_only', 'Ship directly from stock (Ship only)'),
2283 ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
2284 ('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),
2285 'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'),
2286 'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'),
2287 'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'),
2288 'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'),
2289 'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'),
2290 'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'),
2291 'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'),
2292 'out_type_id': fields.many2one('stock.picking.type', 'Out Type'),
2293 'in_type_id': fields.many2one('stock.picking.type', 'In Type'),
2294 'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'),
2295 'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'),
2296 'reception_route_id': fields.many2one('stock.location.route', 'Reception Route'),
2297 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'),
2298 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'),
2299 'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'),
2300 'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes'),
2301 'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse'),
2304 def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None):
2305 resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))])
2306 if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id
2307 resupply_wh_ids.add(default_resupply_wh_id)
2308 resupply_wh_ids = list(resupply_wh_ids)
2309 return {'value': {'resupply_wh_ids': resupply_wh_ids}}
2311 def _get_inter_wh_location(self, cr, uid, warehouse, context=None):
2312 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2313 data_obj = self.pool.get('ir.model.data')
2315 inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1]
2317 inter_wh_loc = False
2320 def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None):
2321 return self.pool.get('product.product').search(cr, uid, [], context=context)
2323 def _assign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2324 product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2325 self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2327 def _unassign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2328 product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2329 self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(3, inter_wh_route_id)]}, context=context)
2331 def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2333 'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2334 'warehouse_selectable': False,
2335 'product_selectable': True,
2336 'product_categ_selectable': True,
2337 'supplied_wh_id': warehouse.id,
2338 'supplier_wh_id': wh.id,
2341 def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2342 location_obj = self.pool.get('stock.location')
2343 route_obj = self.pool.get('stock.location.route')
2344 pull_obj = self.pool.get('procurement.rule')
2345 #create route selectable on the product to resupply the warehouse from another one
2346 inter_wh_location_id = self._get_inter_wh_location(cr, uid, warehouse, context=context)
2347 if inter_wh_location_id:
2348 input_loc = warehouse.wh_input_stock_loc_id
2349 if warehouse.reception_steps == 'one_step':
2350 input_loc = warehouse.lot_stock_id
2351 inter_wh_location = location_obj.browse(cr, uid, inter_wh_location_id, context=context)
2352 for wh in supplier_warehouses:
2353 output_loc = wh.wh_output_stock_loc_id
2354 if wh.delivery_steps == 'ship_only':
2355 output_loc = wh.lot_stock_id
2356 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2357 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2358 values = [(output_loc, inter_wh_location, wh.out_type_id.id, wh), (inter_wh_location, input_loc, warehouse.in_type_id.id, warehouse)]
2359 pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context)
2360 for pull_rule in pull_rules_list:
2361 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2362 #if the warehouse is also set as default resupply method, assign this route automatically to all product
2363 if default_resupply_wh and default_resupply_wh.id == wh.id:
2364 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2365 #finally, save the route on the warehouse
2366 self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2368 def _default_stock_id(self, cr, uid, context=None):
2369 #lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock')
2371 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2372 return warehouse.lot_stock_id.id
2377 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2378 'lot_stock_id': _default_stock_id,
2379 'reception_steps': 'one_step',
2380 'delivery_steps': 'ship_only',
2382 _sql_constraints = [
2383 ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2384 ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2385 ('default_resupply_wh_diff', 'check (id != default_resupply_wh_id)', 'The default resupply warehouse should be different that the warehouse itself!'),
2388 def _get_partner_locations(self, cr, uid, ids, context=None):
2389 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2390 data_obj = self.pool.get('ir.model.data')
2391 location_obj = self.pool.get('stock.location')
2393 customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2394 supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2396 customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2397 customer_loc = customer_loc and customer_loc[0] or False
2398 supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2399 supplier_loc = supplier_loc and supplier_loc[0] or False
2400 if not (customer_loc and supplier_loc):
2401 raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2402 return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2404 def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2405 location_obj = self.pool.get('stock.location')
2407 new_reception_step = new_reception_step or warehouse.reception_steps
2408 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2409 if warehouse.reception_steps != new_reception_step:
2410 location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2411 if new_reception_step != 'one_step':
2412 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2413 if new_reception_step == 'three_steps':
2414 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2416 if warehouse.delivery_steps != new_delivery_step:
2417 location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
2418 if new_delivery_step != 'ship_only':
2419 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
2420 if new_delivery_step == 'pick_pack_ship':
2421 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
2424 def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
2426 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2427 'product_categ_selectable': True,
2428 'product_selectable': False,
2432 def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None):
2433 pull_rules_list = []
2434 for from_loc, dest_loc, pick_type_id, warehouse in values:
2435 pull_rules_list.append({
2436 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2437 'location_src_id': from_loc.id,
2438 'location_id': dest_loc.id,
2439 'route_id': new_route_id,
2441 'picking_type_id': pick_type_id,
2442 'procure_method': 'make_to_order',
2443 'warehouse_id': supplied_warehouse.id,
2444 'propagate_warehouse_id': warehouse.id,
2446 return pull_rules_list
2448 def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
2450 push_rules_list = []
2451 pull_rules_list = []
2452 for from_loc, dest_loc, pick_type_id in values:
2453 push_rules_list.append({
2454 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2455 'location_from_id': from_loc.id,
2456 'location_dest_id': dest_loc.id,
2457 'route_id': new_route_id,
2459 'picking_type_id': pick_type_id,
2461 'warehouse_id': warehouse.id,
2463 pull_rules_list.append({
2464 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2465 'location_src_id': from_loc.id,
2466 'location_id': dest_loc.id,
2467 'route_id': new_route_id,
2469 'picking_type_id': pick_type_id,
2470 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
2472 'warehouse_id': warehouse.id,
2475 return push_rules_list, pull_rules_list
2477 def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
2478 route_obj = self.pool.get('stock.location.route')
2479 data_obj = self.pool.get('ir.model.data')
2481 mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
2483 mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('MTO'))], context=context)
2484 mto_route_id = mto_route_id and mto_route_id[0] or False
2485 if not mto_route_id:
2486 raise osv.except_osv(_('Error!'), _('Can\'t find any generic MTO route.'))
2488 from_loc, dest_loc, pick_type_id = values[0]
2490 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
2491 'location_src_id': from_loc.id,
2492 'location_id': dest_loc.id,
2493 'route_id': mto_route_id,
2495 'picking_type_id': pick_type_id,
2496 'procure_method': 'make_to_order',
2498 'warehouse_id': warehouse.id,
2501 def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
2503 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2504 'warehouse_selectable': False,
2505 'product_selectable': True,
2506 'product_categ_selectable': True,
2507 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
2511 def create_routes(self, cr, uid, ids, warehouse, context=None):
2513 route_obj = self.pool.get('stock.location.route')
2514 pull_obj = self.pool.get('procurement.rule')
2515 push_obj = self.pool.get('stock.location.path')
2516 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2517 #create reception route and rules
2518 route_name, values = routes_dict[warehouse.reception_steps]
2519 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2520 reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
2521 wh_route_ids.append((4, reception_route_id))
2522 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
2523 #create the push/pull rules
2524 for push_rule in push_rules_list:
2525 push_obj.create(cr, uid, vals=push_rule, context=context)
2526 for pull_rule in pull_rules_list:
2527 #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
2528 pull_rule['procure_method'] = 'make_to_order'
2529 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2531 #create MTS route and pull rules for delivery a specific route MTO to be set on the product
2532 route_name, values = routes_dict[warehouse.delivery_steps]
2533 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2534 #create the route and its pull rules
2535 delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
2536 wh_route_ids.append((4, delivery_route_id))
2537 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
2538 for pull_rule in pull_rules_list:
2539 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2540 #create MTO pull rule and link it to the generic MTO route
2541 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2542 mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
2544 #create a route for cross dock operations, that can be set on products and product categories
2545 route_name, values = routes_dict['crossdock']
2546 crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
2547 crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
2548 wh_route_ids.append((4, crossdock_route_id))
2549 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)
2550 for pull_rule in pull_rules_list:
2551 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2553 #create route selectable on the product to resupply the warehouse from another one
2554 self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
2556 #return routes and mto pull rule to store on the warehouse
2558 'route_ids': wh_route_ids,
2559 'mto_pull_id': mto_pull_id,
2560 'reception_route_id': reception_route_id,
2561 'delivery_route_id': delivery_route_id,
2562 'crossdock_route_id': crossdock_route_id,
2565 def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2566 picking_type_obj = self.pool.get('stock.picking.type')
2567 pull_obj = self.pool.get('procurement.rule')
2568 push_obj = self.pool.get('stock.location.path')
2569 route_obj = self.pool.get('stock.location.route')
2570 new_reception_step = new_reception_step or warehouse.reception_steps
2571 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2573 #change the default source and destination location and (de)activate picking types
2574 input_loc = warehouse.wh_input_stock_loc_id
2575 if new_reception_step == 'one_step':
2576 input_loc = warehouse.lot_stock_id
2577 output_loc = warehouse.wh_output_stock_loc_id
2578 if new_delivery_step == 'ship_only':
2579 output_loc = warehouse.lot_stock_id
2580 picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
2581 picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
2582 picking_type_obj.write(cr, uid, warehouse.int_type_id.id, {'active': new_reception_step != 'one_step'}, context=context)
2583 picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
2584 picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
2586 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2587 #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
2588 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
2589 route_name, values = routes_dict[new_delivery_step]
2590 route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2591 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
2592 #create the pull rules
2593 for pull_rule in pull_rules_list:
2594 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2596 #update reception route and rules: unlink the existing rules of the warehouse reception route and recreate it
2597 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
2598 push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
2599 route_name, values = routes_dict[new_reception_step]
2600 route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2601 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
2602 #create the push/pull rules
2603 for push_rule in push_rules_list:
2604 push_obj.create(cr, uid, vals=push_rule, context=context)
2605 for pull_rule in pull_rules_list:
2606 #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
2607 pull_rule['procure_method'] = 'make_to_order'
2608 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2610 route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
2613 dummy, values = routes_dict[new_delivery_step]
2614 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2615 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
2618 def create(self, cr, uid, vals, context=None):
2623 data_obj = self.pool.get('ir.model.data')
2624 seq_obj = self.pool.get('ir.sequence')
2625 picking_type_obj = self.pool.get('stock.picking.type')
2626 location_obj = self.pool.get('stock.location')
2628 #create view location for warehouse
2629 wh_loc_id = location_obj.create(cr, uid, {
2630 'name': _(vals.get('name')),
2632 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1]
2634 vals['view_location_id'] = wh_loc_id
2635 #create all location
2636 def_values = self.default_get(cr, uid, {'reception_steps', 'delivery_steps'})
2637 reception_steps = vals.get('reception_steps', def_values['reception_steps'])
2638 delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
2639 context_with_inactive = context.copy()
2640 context_with_inactive['active_test'] = False
2642 {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
2643 {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
2644 {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
2645 {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
2646 {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
2648 for values in sub_locations:
2649 location_id = location_obj.create(cr, uid, {
2650 'name': values['name'],
2651 'usage': 'internal',
2652 'location_id': wh_loc_id,
2653 'active': values['active'],
2654 }, context=context_with_inactive)
2655 vals[values['field']] = location_id
2657 #create new sequences
2658 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)
2659 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)
2660 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)
2661 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)
2662 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)
2665 new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
2667 warehouse = self.browse(cr, uid, new_id, context=context)
2668 wh_stock_loc = warehouse.lot_stock_id
2669 wh_input_stock_loc = warehouse.wh_input_stock_loc_id
2670 wh_output_stock_loc = warehouse.wh_output_stock_loc_id
2671 wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
2673 #fetch customer and supplier locations, for references
2674 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context)
2676 #create in, out, internal picking types for warehouse
2677 input_loc = wh_input_stock_loc
2678 if warehouse.reception_steps == 'one_step':
2679 input_loc = wh_stock_loc
2680 output_loc = wh_output_stock_loc
2681 if warehouse.delivery_steps == 'ship_only':
2682 output_loc = wh_stock_loc
2684 #choose the next available color for the picking types of this warehouse
2685 all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
2686 not_used_colors = list(set(range(0, 9)) - set([x['color'] for x in all_used_colors]))
2689 color = not_used_colors[0]
2691 in_type_id = picking_type_obj.create(cr, uid, vals={
2692 'name': _('Receptions'),
2693 'warehouse_id': new_id,
2694 'code_id': 'incoming',
2695 'auto_force_assign': True,
2696 'sequence_id': in_seq_id,
2697 'default_location_src_id': supplier_loc.id,
2698 'default_location_dest_id': input_loc.id,
2699 'color': color}, context=context)
2700 out_type_id = picking_type_obj.create(cr, uid, vals={
2701 'name': _('Delivery Orders'),
2702 'warehouse_id': new_id,
2703 'code_id': 'outgoing',
2704 'sequence_id': out_seq_id,
2706 'default_location_src_id': output_loc.id,
2707 'default_location_dest_id': customer_loc.id,
2708 'color': color}, context=context)
2709 int_type_id = picking_type_obj.create(cr, uid, vals={
2710 'name': _('Internal Transfers'),
2711 'warehouse_id': new_id,
2712 'code_id': 'internal',
2713 'sequence_id': int_seq_id,
2714 'default_location_src_id': wh_stock_loc.id,
2715 'default_location_dest_id': wh_stock_loc.id,
2716 'active': reception_steps != 'one_step',
2718 'color': color}, context=context)
2719 pack_type_id = picking_type_obj.create(cr, uid, vals={
2721 'warehouse_id': new_id,
2722 'code_id': 'internal',
2723 'sequence_id': pack_seq_id,
2724 'default_location_src_id': wh_pack_stock_loc.id,
2725 'default_location_dest_id': output_loc.id,
2726 'active': delivery_steps == 'pick_pack_ship',
2728 'color': color}, context=context)
2729 pick_type_id = picking_type_obj.create(cr, uid, vals={
2731 'warehouse_id': new_id,
2732 'code_id': 'internal',
2733 'sequence_id': pick_seq_id,
2734 'default_location_src_id': wh_stock_loc.id,
2735 'default_location_dest_id': wh_pack_stock_loc.id,
2736 'active': delivery_steps != 'ship_only',
2738 'color': color}, context=context)
2740 #write picking types on WH
2742 'in_type_id': in_type_id,
2743 'out_type_id': out_type_id,
2744 'pack_type_id': pack_type_id,
2745 'pick_type_id': pick_type_id,
2746 'int_type_id': int_type_id,
2748 super(stock_warehouse, self).write(cr, uid, new_id, vals=vals, context=context)
2751 #create routes and push/pull rules
2752 new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
2753 self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
2756 def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
2757 return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name
2759 def _format_routename(self, cr, uid, obj, name, context=None):
2760 return obj.name + ': ' + name
2762 def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
2763 #fetch customer and supplier locations, for references
2764 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
2767 'one_step': (_('Reception in 1 step'), []),
2768 'two_steps': (_('Reception in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2769 '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)]),
2770 '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)]),
2771 'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
2772 '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)]),
2773 '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)]),
2776 def _handle_renaming(self, cr, uid, warehouse, name, context=None):
2777 location_obj = self.pool.get('stock.location')
2778 route_obj = self.pool.get('stock.location.route')
2779 pull_obj = self.pool.get('procurement.rule')
2780 push_obj = self.pool.get('stock.location.path')
2782 location_id = warehouse.lot_stock_id.location_id.id
2783 location_obj.write(cr, uid, location_id, {'name': name}, context=context)
2784 #rename route and push-pull rules
2785 for route in warehouse.route_ids:
2786 route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context)
2787 for pull in route.pull_ids:
2788 pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2789 for push in route.push_ids:
2790 push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2791 #change the mto pull rule name
2792 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
2794 def write(self, cr, uid, ids, vals, context=None):
2797 if isinstance(ids, (int, long)):
2799 seq_obj = self.pool.get('ir.sequence')
2800 route_obj = self.pool.get('stock.location.route')
2801 warehouse_obj = self.pool.get('stock.warehouse')
2803 context_with_inactive = context.copy()
2804 context_with_inactive['active_test'] = False
2805 for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
2806 #first of all, check if we need to delete and recreate route
2807 if vals.get('reception_steps') or vals.get('delivery_steps'):
2808 #activate and deactivate location according to reception and delivery option
2809 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
2810 # switch between route
2811 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
2812 if vals.get('code') or vals.get('name'):
2813 name = warehouse.name
2815 if vals.get('name'):
2816 name = vals.get('name')
2817 self._handle_renaming(cr, uid, warehouse, name, context=context_with_inactive)
2818 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)
2819 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)
2820 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)
2821 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)
2822 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)
2823 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
2824 for cmd in vals.get('resupply_wh_ids'):
2826 new_ids = set(cmd[2])
2827 old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
2828 to_add_wh_ids = new_ids - old_ids
2830 supplier_warehouses = warehouse_obj.browse(cr, uid, list(to_add_wh_ids), context=context)
2831 self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
2832 to_remove_wh_ids = old_ids - new_ids
2833 if to_remove_wh_ids:
2834 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)
2835 if to_remove_route_ids:
2836 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
2840 if 'default_resupply_wh_id' in vals:
2841 if warehouse.default_resupply_wh_id:
2842 #remove the existing resupplying route on all products
2843 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)
2844 for inter_wh_route_id in to_remove_route_ids:
2845 self._unassign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2846 if vals.get('default_resupply_wh_id'):
2847 #assign the new resupplying route on all products
2848 to_assign_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', vals.get('default_resupply_wh_id'))], context=context)
2849 for inter_wh_route_id in to_assign_route_ids:
2850 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2852 return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
2854 def unlink(self, cr, uid, ids, context=None):
2855 #TODO try to delete location and route and if not possible, put them in inactive
2856 return super(stock_warehouse, self).unlink(cr, uid, ids, context=context)
2858 def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
2859 all_routes = [route.id for route in warehouse.route_ids]
2860 all_routes += [warehouse.mto_pull_id.route_id.id]
2863 def view_all_routes_for_wh(self, cr, uid, ids, context=None):
2865 for wh in self.browse(cr, uid, ids, context=context):
2866 all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
2868 domain = [('id', 'in', all_routes)]
2870 'name': _('Warehouse\'s Routes'),
2872 'res_model': 'stock.location.route',
2873 'type': 'ir.actions.act_window',
2875 'view_mode': 'tree,form',
2876 'view_type': 'form',
2880 class stock_location_path(osv.osv):
2881 _name = "stock.location.path"
2882 _description = "Pushed Flows"
2885 def _get_route(self, cr, uid, ids, context=None):
2886 #WARNING TODO route_id is not required, so a field related seems a bad idea >-<
2892 context_with_inactive = context.copy()
2893 context_with_inactive['active_test'] = False
2894 for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive):
2895 for push_rule in route.push_ids:
2896 result[push_rule.id] = True
2897 return result.keys()
2900 'name': fields.char('Operation Name', size=64, required=True),
2901 'company_id': fields.many2one('res.company', 'Company'),
2902 'route_id': fields.many2one('stock.location.route', 'Route'),
2903 'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
2904 'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
2905 'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
2906 'invoice_state': fields.selection([
2907 ("invoiced", "Invoiced"),
2908 ("2binvoiced", "To Be Invoiced"),
2909 ("none", "Not Applicable")], "Invoice Status",
2911 '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"),
2912 'auto': fields.selection(
2913 [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
2915 required=True, select=1,
2916 help="This is used to define paths the product has to follow within the location tree.\n" \
2917 "The 'Automatic Move' value will create a stock move after the current one that will be "\
2918 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
2919 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
2921 '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'),
2922 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={
2923 'stock.location.route': (_get_route, ['active'], 20),
2924 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),},
2925 help="If the active field is set to False, it will allow you to hide the rule without removing it." ),
2926 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
2931 'invoice_state': 'none',
2932 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
2936 def _apply(self, cr, uid, rule, move, context=None):
2937 move_obj = self.pool.get('stock.move')
2938 newdate = (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta.relativedelta(days=rule.delay or 0)).strftime('%Y-%m-%d')
2939 if rule.auto=='transparent':
2940 move_obj.write(cr, uid, [move.id], {
2942 'location_dest_id': rule.location_dest_id.id
2944 if rule.location_dest_id.id<>move.location_dest_id.id:
2945 move_obj._push_apply(self, cr, uid, move.id, context)
2948 move_id = move_obj.copy(cr, uid, move.id, {
2949 'location_id': move.location_dest_id.id,
2950 'location_dest_id': rule.location_dest_id.id,
2951 'date': datetime.now().strftime('%Y-%m-%d'),
2952 'company_id': rule.company_id and rule.company_id.id or False,
2953 'date_expected': newdate,
2954 'picking_id': False,
2955 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
2957 'propagate': rule.propagate,
2958 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
2960 move_obj.write(cr, uid, [move.id], {
2961 'move_dest_id': move_id,
2963 move_obj.action_confirm(cr, uid, [move_id], context=None)
2966 class stock_move_putaway(osv.osv):
2967 _name = 'stock.move.putaway'
2968 _description = 'Proposed Destination'
2970 'move_id': fields.many2one('stock.move', required=True),
2971 'location_id': fields.many2one('stock.location', 'Location', required=True),
2972 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
2973 'quantity': fields.float('Quantity', required=True),
2978 # -------------------------
2979 # Packaging related stuff
2980 # -------------------------
2982 from openerp.report import report_sxw
2983 report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package', 'addons/stock/report/picking_barcode.rml')
2985 class stock_package(osv.osv):
2987 These are the packages, containing quants and/or other packages
2989 _name = "stock.quant.package"
2990 _description = "Physical Packages"
2991 _parent_name = "parent_id"
2992 _parent_store = True
2993 _parent_order = 'name'
2994 _order = 'parent_left'
2996 def name_get(self, cr, uid, ids, context=None):
2997 res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
3000 def _complete_name(self, cr, uid, ids, name, args, context=None):
3001 """ Forms complete name of location from parent location to child location.
3002 @return: Dictionary of values
3005 for m in self.browse(cr, uid, ids, context=context):
3007 parent = m.parent_id
3009 res[m.id] = parent.name + ' / ' + res[m.id]
3010 parent = parent.parent_id
3013 def _get_packages(self, cr, uid, ids, context=None):
3014 """Returns packages from quants for store"""
3016 for quant in self.browse(cr, uid, ids, context=context):
3017 if quant.package_id:
3018 res.add(quant.package_id.id)
3021 def _get_packages_to_relocate(self, cr, uid, ids, context=None):
3023 for pack in self.browse(cr, uid, ids, context=context):
3026 res.add(pack.parent_id.id)
3029 # TODO: Problem when package is empty!
3031 def _get_package_info(self, cr, uid, ids, name, args, context=None):
3032 default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
3033 res = {}.fromkeys(ids, {'location_id': False, 'company_id': default_company_id})
3034 for pack in self.browse(cr, uid, ids, context=context):
3036 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
3037 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
3038 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
3039 elif pack.children_ids:
3040 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
3041 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
3042 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
3046 'name': fields.char('Package Reference', size=64, select=True),
3047 'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
3048 'parent_left': fields.integer('Left Parent', select=1),
3049 'parent_right': fields.integer('Right Parent', select=1),
3050 'packaging_id': fields.many2one('product.packaging', 'Type of Packaging'),
3051 'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
3053 'stock.quant': (_get_packages, ['location_id'], 10),
3054 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3056 'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content'),
3057 'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict'),
3058 'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages'),
3059 'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package",
3061 'stock.quant': (_get_packages, ['company_id'], 10),
3062 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3064 'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
3066 'stock.quant': (_get_packages, ['owner_id'], 10),
3067 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3071 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3073 def _check_location(self, cr, uid, ids, context=None):
3074 '''checks that all quants in a package are stored in the same location'''
3075 quant_obj = self.pool.get('stock.quant')
3076 for pack in self.browse(cr, uid, ids, context=context):
3078 while parent.parent_id:
3079 parent = parent.parent_id
3080 quant_ids = self.get_content(cr, uid, [parent.id], context=context)
3081 quants = quant_obj.browse(cr, uid, quant_ids, context=context)
3082 location_id = quants and quants[0].location_id.id or False
3083 if not all([quant.location_id.id == location_id for quant in quants]):
3088 (_check_location, 'Everything inside a package should be in the same location', ['location_id']),
3091 def action_print(self, cr, uid, ids, context=None):
3095 'ids': context.get('active_id') and [context.get('active_id')] or ids,
3096 'model': 'stock.quant.package',
3097 'form': self.read(cr, uid, ids)[0]
3100 'type': 'ir.actions.report.xml',
3101 'report_name': 'stock.quant.package.barcode',
3105 def unpack(self, cr, uid, ids, context=None):
3106 quant_obj = self.pool.get('stock.quant')
3107 for package in self.browse(cr, uid, ids, context=context):
3108 quant_ids = [quant.id for quant in package.quant_ids]
3109 quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3110 children_package_ids = [child_package.id for child_package in package.children_ids]
3111 self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3112 #delete current package since it contains nothing anymore
3113 self.unlink(cr, uid, ids, context=context)
3114 return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3116 def get_content(self, cr, uid, ids, context=None):
3117 child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3118 return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3120 def get_content_package(self, cr, uid, ids, context=None):
3121 quants_ids = self.get_content(cr, uid, ids, context=context)
3122 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3123 res['domain'] = [('id', 'in', quants_ids)]
3126 def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3127 ''' find the total of given product 'product_id' inside the given package 'package_id'''
3128 quant_obj = self.pool.get('stock.quant')
3129 all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3131 for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3132 if quant.product_id.id == product_id:
3137 class stock_pack_operation(osv.osv):
3138 _name = "stock.pack.operation"
3139 _description = "Packing Operation"
3141 def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3143 for ops in self.browse(cr, uid, ids, context=context):
3144 qty = ops.product_qty
3145 for quant in ops.reserved_quant_ids:
3150 def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3151 res = self.on_change_tests(cr, uid, ids, product_id, product_uom_id, product_qty, context=context)
3152 if product_id and not product_uom_id:
3153 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3154 res['value']['product_uom_id'] = product.uom_id.id
3157 def on_change_tests(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3159 uom_obj = self.pool.get('product.uom')
3161 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3162 product_uom_id = product_uom_id or product.uom_id.id
3163 selected_uom = uom_obj.browse(cr, uid, product_uom_id, context=context)
3164 if selected_uom.category_id.id != product.uom_id.category_id.id:
3166 'title': _('Warning: wrong UoM!'),
3167 'message': _('The selected UoM for product %s is not compatible with the UoM set on the product form. \nPlease choose an UoM within the same UoM category.') % (product.name)
3169 if product_qty and 'warning' not in res:
3170 rounded_qty = uom_obj._compute_qty(cr, uid, product_uom_id, product_qty, product_uom_id, round=True)
3171 if rounded_qty != product_qty:
3173 'title': _('Warning: wrong quantity!'),
3174 'message': _('The chosen quantity for product %s is not compatible with the UoM rounding. It will be automatically converted at confirmation') % (product.name)
3179 'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3180 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
3181 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3182 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3183 'package_id': fields.many2one('stock.quant.package', 'Package'), # 2
3184 'quant_id': fields.many2one('stock.quant', 'Quant'), # 3
3185 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3186 'result_package_id': fields.many2one('stock.quant.package', 'Container Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3187 'date': fields.datetime('Date', required=True),
3188 'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3189 #'update_cost': fields.boolean('Need cost update'),
3190 'cost': fields.float("Cost", help="Unit Cost for this product line"),
3191 'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3192 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_op_id', string='Reserved Quants', readonly=True, help='Quants reserved for this operation'),
3193 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'),
3197 'date': fields.date.context_today,
3200 def _get_domain(self, cr, uid, ops, context=None):
3202 Gives domain for different
3206 res.append(('package_id', '=', ops.package_id.id), )
3208 res.append(('lot_id', '=', ops.lot_id.id), )
3210 res.append(('owner_id', '=', ops.owner_id.id), )
3212 res.append(('owner_id', '=', False), )
3215 #TODO: this function can be refactored
3216 def _search_and_increment(self, cr, uid, picking_id, key, context=None):
3217 '''Search for an operation on an existing key in a picking, if it exists increment the qty (+1) otherwise create it
3219 :param key: tuple directly reusable in a domain
3220 context can receive a key 'current_package_id' with the package to consider for this operation
3223 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
3224 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
3225 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
3226 (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)
3228 quant_obj = self.pool.get('stock.quant')
3232 #if current_package_id is given in the context, we increase the number of items in this package
3233 package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3234 existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id), key] + package_clause, context=context)
3235 if existing_operation_ids:
3236 #existing operation found for the given key and picking => increment its quantity
3237 operation_id = existing_operation_ids[0]
3238 qty = self.browse(cr, uid, operation_id, context=context).product_qty + 1
3239 self.write(cr, uid, operation_id, {'product_qty': qty}, context=context)
3241 #no existing operation found for the given key and picking => create a new one
3242 var_name, dummy, value = key
3244 if var_name == 'product_id':
3245 uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
3246 elif var_name == 'quant_id':
3247 quant = quant_obj.browse(cr, uid, value, context=context)
3248 uom_id = quant.product_id.uom_id.id
3250 'picking_id': picking_id,
3253 'product_uom_id': uom_id,
3255 operation_id = self.create(cr, uid, values, context=context)
3256 values.update({'id': operation_id})
3259 class stock_warehouse_orderpoint(osv.osv):
3261 Defines Minimum stock rules.
3263 _name = "stock.warehouse.orderpoint"
3264 _description = "Minimum Inventory Rule"
3266 def get_draft_procurements(self, cr, uid, ids, context=None):
3270 if not isinstance(ids, list):
3272 procurement_obj = self.pool.get('procurement.order')
3273 for orderpoint in self.browse(cr, uid, ids, context=context):
3274 procurement_ids = procurement_obj.search(cr, uid, [('state', 'not in', ('cancel', 'done')), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)], context=context)
3275 return list(set(procurement_ids))
3277 def _check_product_uom(self, cr, uid, ids, context=None):
3279 Check if the UoM has the same category as the product standard UoM
3284 for rule in self.browse(cr, uid, ids, context=context):
3285 if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
3290 def action_view_proc_to_process(self, cr, uid, ids, context=None):
3291 act_obj = self.pool.get('ir.actions.act_window')
3292 mod_obj = self.pool.get('ir.model.data')
3293 draft_ids = self.get_draft_procurements(cr, uid, ids, context=context)
3294 result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
3298 result = act_obj.read(cr, uid, [result[1]], context=context)[0]
3299 result['domain'] = "[('id', 'in', [" + ','.join(map(str, draft_ids)) + "])]"
3303 'name': fields.char('Name', size=32, required=True),
3304 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
3305 'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
3306 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
3307 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
3308 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '!=', 'service')]),
3309 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
3310 'product_min_qty': fields.float('Minimum Quantity', required=True,
3311 help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
3312 "a procurement to bring the forecasted quantity to the Max Quantity."),
3313 'product_max_qty': fields.float('Maximum Quantity', required=True,
3314 help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
3315 "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
3316 'qty_multiple': fields.integer('Qty Multiple', required=True,
3317 help="The procurement quantity will be rounded up to this multiple."),
3318 'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
3319 'company_id': fields.many2one('res.company', 'Company', required=True)
3322 'active': lambda *a: 1,
3323 'logic': lambda *a: 'max',
3324 'qty_multiple': lambda *a: 1,
3325 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3326 'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
3327 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
3329 _sql_constraints = [
3330 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
3333 (_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']),
3336 def default_get(self, cr, uid, fields, context=None):
3337 res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
3338 # default 'warehouse_id' and 'location_id'
3339 if 'warehouse_id' not in res:
3340 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0', context)
3341 res['warehouse_id'] = warehouse.id
3342 if 'location_id' not in res:
3343 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, res['warehouse_id'], context)
3344 res['location_id'] = warehouse.lot_stock_id.id
3347 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
3348 """ Finds location id for changed warehouse.
3349 @param warehouse_id: Changed id of warehouse.
3350 @return: Dictionary of values.
3353 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
3354 v = {'location_id': w.lot_stock_id.id}
3358 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
3359 """ Finds UoM for changed product.
3360 @param product_id: Changed id of product.
3361 @return: Dictionary of values.
3364 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3365 d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
3366 v = {'product_uom': prod.uom_id.id}
3367 return {'value': v, 'domain': d}
3368 return {'domain': {'product_uom': []}}
3370 def copy(self, cr, uid, id, default=None, context=None):
3374 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3376 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
3380 class stock_picking_type(osv.osv):
3381 _name = "stock.picking.type"
3382 _description = "The picking type determines the picking view"
3384 def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
3385 """ Generic method to generate data for bar chart values using SparklineBarWidget.
3386 This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
3388 :param obj: the target model (i.e. crm_lead)
3389 :param domain: the domain applied to the read_group
3390 :param list read_fields: the list of fields to read in the read_group
3391 :param str value_field: the field used to compute the value of the bar slice
3392 :param str groupby_field: the fields used to group
3394 :return list section_result: a list of dicts: [
3395 { 'value': (int) bar_column_value,
3396 'tootip': (str) bar_column_tooltip,
3400 month_begin = date.today().replace(day=1)
3403 'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
3404 } for i in range(10, -1, -1)]
3405 group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
3406 for group in group_obj:
3407 group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
3408 month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
3409 section_result[10 - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
3410 return section_result
3412 def _get_picking_data(self, cr, uid, ids, field_name, arg, context=None):
3413 obj = self.pool.get('stock.picking')
3414 res = dict.fromkeys(ids, False)
3415 month_begin = date.today().replace(day=1)
3416 groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3417 groupby_end = (month_begin + relativedelta.relativedelta(months=3)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3420 ('picking_type_id', '=', id),
3421 ('state', 'not in', ['done', 'cancel']),
3422 ('date', '>=', groupby_begin),
3423 ('date', '<', groupby_end),
3425 res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['date'], 'picking_type_id_count', 'date', context=context)
3428 def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
3429 obj = self.pool.get('stock.picking')
3431 'count_picking_draft': [('state', '=', 'draft')],
3432 'count_picking_waiting': [('state','=','confirmed')],
3433 'count_picking_ready': [('state','=','assigned')],
3434 'count_picking': [('state','in',('assigned','waiting','confirmed'))],
3435 'count_picking_late': [('min_date','<', time.strftime('%Y-%m-%d %H:%M:%S')), ('state','in',('assigned','waiting','confirmed'))],
3436 'count_picking_backorders': [('backorder_id','<>', False), ('state','!=','done')],
3439 for field in domains:
3440 data = obj.read_group(cr, uid, domains[field] +
3441 [('state', 'not in',('done','cancel')), ('picking_type_id', 'in', ids)],
3442 ['picking_type_id'], ['picking_type_id'], context=context)
3443 count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
3445 result.setdefault(tid, {})[field] = count.get(tid, 0)
3447 if result[tid]['count_picking']:
3448 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] *100 / result[tid]['count_picking']
3449 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] *100 / (result[tid]['count_picking'] + result[tid]['count_picking_draft'])
3451 result[tid]['rate_picking_late'] = 0
3452 result[tid]['rate_picking_backorders'] = 0
3455 #TODO: not returning valus in required format to show in sparkline library,just added latest_picking_waiting need to add proper logic.
3456 def _get_picking_history(self, cr, uid, ids, field_names, arg, context=None):
3457 obj = self.pool.get('stock.picking')
3461 'latest_picking_late': [],
3462 'latest_picking_backorders': [],
3463 'latest_picking_waiting': []
3466 pick_ids = obj.search(cr, uid, [('state', '=','done'), ('picking_type_id','=',type_id)], limit=12, order="date desc", context=context)
3467 for pick in obj.browse(cr, uid, pick_ids, context=context):
3468 result[type_id]['latest_picking_late'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3469 result[type_id]['latest_picking_backorders'] = bool(pick.backorder_id)
3470 result[type_id]['latest_picking_waiting'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3473 def onchange_picking_code(self, cr, uid, ids, picking_code=False):
3474 if not picking_code:
3477 obj_data = self.pool.get('ir.model.data')
3478 stock_loc = obj_data.get_object_reference(cr, uid, 'stock','stock_location_stock')[1]
3481 'default_location_src_id': stock_loc,
3482 'default_location_dest_id': stock_loc,
3484 if picking_code == 'incoming':
3485 result['default_location_src_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_suppliers')[1]
3486 return {'value': result}
3487 if picking_code == 'outgoing':
3488 result['default_location_dest_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_customers')[1]
3489 return {'value': result}
3491 return {'value': result}
3493 def _get_name(self, cr, uid, ids, field_names, arg, context=None):
3494 return dict(self.name_get(cr, uid, ids, context=context))
3496 def name_get(self, cr, uid, ids, context=None):
3497 """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
3500 if not isinstance(ids, list):
3505 for record in self.browse(cr, uid, ids, context=context):
3507 if record.warehouse_id:
3508 name = record.warehouse_id.name + ': ' +name
3509 if context.get('special_shortened_wh_name'):
3510 if record.warehouse_id:
3511 name = record.warehouse_id.name
3513 name = _('Customer') + ' (' + record.name + ')'
3514 res.append((record.id, name))
3517 def _default_warehouse(self, cr, uid, context=None):
3518 user = self.pool.get('res.users').browse(cr, uid, uid, context)
3519 res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
3520 return res and res[0] or False
3523 'name': fields.char('Name', translate=True, required=True),
3524 'complete_name': fields.function(_get_name, type='char', string='Name'),
3525 'pack': fields.boolean('Prefill Pack Operations', help='This picking type needs packing interface'),
3526 'auto_force_assign': fields.boolean('Automatic Availability', help='This picking type does\'t need to check for the availability in source location.'),
3527 'color': fields.integer('Color'),
3528 'delivery': fields.boolean('Print delivery'),
3529 'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
3530 'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
3531 'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
3532 #TODO: change field name to "code" as it's not a many2one anymore
3533 'code_id': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Picking type code', required=True),
3534 'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
3535 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
3536 'active': fields.boolean('Active'),
3538 # Statistics for the kanban view
3539 'weekly_picking': fields.function(_get_picking_data,
3541 string='Scheduled pickings per week'),
3543 'count_picking_draft': fields.function(_get_picking_count,
3544 type='integer', multi='_get_picking_count'),
3545 'count_picking_ready': fields.function(_get_picking_count,
3546 type='integer', multi='_get_picking_count'),
3547 'count_picking': fields.function(_get_picking_count,
3548 type='integer', multi='_get_picking_count'),
3549 'count_picking_waiting': fields.function(_get_picking_count,
3550 type='integer', multi='_get_picking_count'),
3551 'count_picking_late': fields.function(_get_picking_count,
3552 type='integer', multi='_get_picking_count'),
3553 'count_picking_backorders': fields.function(_get_picking_count,
3554 type='integer', multi='_get_picking_count'),
3556 'rate_picking_late': fields.function(_get_picking_count,
3557 type='integer', multi='_get_picking_count'),
3558 'rate_picking_backorders': fields.function(_get_picking_count,
3559 type='integer', multi='_get_picking_count'),
3561 'latest_picking_late': fields.function(_get_picking_history,
3562 type='string', multi='_get_picking_history'),
3563 'latest_picking_backorders': fields.function(_get_picking_history,
3564 type='string', multi='_get_picking_history'),
3565 'latest_picking_waiting': fields.function(_get_picking_history,
3566 type='string', multi='_get_picking_history'),
3570 'warehouse_id': _default_warehouse,
3575 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: