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.float_utils import float_compare, float_round
29 from openerp.tools.translate import _
30 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
31 from openerp import SUPERUSER_ID, api
32 import openerp.addons.decimal_precision as dp
33 from openerp.addons.procurement import procurement
37 _logger = logging.getLogger(__name__)
38 #----------------------------------------------------------
40 #----------------------------------------------------------
41 class stock_incoterms(osv.osv):
42 _name = "stock.incoterms"
43 _description = "Incoterms"
45 'name': fields.char('Name', 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."),
46 'code': fields.char('Code', size=3, required=True, help="Incoterm Standard Code"),
47 'active': fields.boolean('Active', help="By unchecking the active field, you may hide an INCOTERM you will not use."),
53 #----------------------------------------------------------
55 #----------------------------------------------------------
57 class stock_location(osv.osv):
58 _name = "stock.location"
59 _description = "Inventory Locations"
60 _parent_name = "location_id"
62 _parent_order = 'name'
63 _order = 'parent_left'
64 _rec_name = 'complete_name'
66 def _location_owner(self, cr, uid, location, context=None):
67 ''' Return the company owning the location if any '''
68 return location and (location.usage == 'internal') and location.company_id or False
70 def _complete_name(self, cr, uid, ids, name, args, context=None):
71 """ Forms complete name of location from parent location to child location.
72 @return: Dictionary of values
75 for m in self.browse(cr, uid, ids, context=context):
77 parent = m.location_id
79 res[m.id] = parent.name + ' / ' + res[m.id]
80 parent = parent.location_id
83 def _get_sublocations(self, cr, uid, ids, context=None):
84 """ return all sublocations of the given stock locations (included) """
87 context_with_inactive = context.copy()
88 context_with_inactive['active_test'] = False
89 return self.search(cr, uid, [('id', 'child_of', ids)], context=context_with_inactive)
91 def _name_get(self, cr, uid, location, context=None):
93 while location.location_id and location.usage != 'view':
94 location = location.location_id
95 name = location.name + '/' + name
98 def name_get(self, cr, uid, ids, context=None):
100 for location in self.browse(cr, uid, ids, context=context):
101 res.append((location.id, self._name_get(cr, uid, location, context=context)))
105 'name': fields.char('Location Name', required=True, translate=True),
106 'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
107 'usage': fields.selection([
108 ('supplier', 'Supplier Location'),
110 ('internal', 'Internal Location'),
111 ('customer', 'Customer Location'),
112 ('inventory', 'Inventory'),
113 ('procurement', 'Procurement'),
114 ('production', 'Production'),
115 ('transit', 'Transit Location')],
116 'Location Type', required=True,
117 help="""* Supplier Location: Virtual location representing the source location for products coming from your suppliers
118 \n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products
119 \n* Internal Location: Physical locations inside your own warehouses,
120 \n* Customer Location: Virtual location representing the destination location for products sent to your customers
121 \n* Inventory: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)
122 \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.
123 \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
124 \n* Transit Location: Counterpart location that should be used in inter-companies or inter-warehouses operations
126 'complete_name': fields.function(_complete_name, type='char', string="Location Name",
127 store={'stock.location': (_get_sublocations, ['name', 'location_id', 'active'], 10)}),
128 'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
129 'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
131 'partner_id': fields.many2one('res.partner', 'Owner', help="Owner of the location if not internal"),
133 'comment': fields.text('Additional Information'),
134 'posx': fields.integer('Corridor (X)', help="Optional localization details, for information purpose only"),
135 'posy': fields.integer('Shelves (Y)', help="Optional localization details, for information purpose only"),
136 'posz': fields.integer('Height (Z)', help="Optional localization details, for information purpose only"),
138 'parent_left': fields.integer('Left Parent', select=1),
139 'parent_right': fields.integer('Right Parent', select=1),
141 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between companies'),
142 'scrap_location': fields.boolean('Is a Scrap Location?', help='Check this box to allow using this location to put scrapped/damaged goods.'),
143 'removal_strategy_id': fields.many2one('product.removal', 'Removal Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to take the products from, which lot etc. for this location. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."),
144 'putaway_strategy_id': fields.many2one('product.putaway', 'Put Away Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to store the products. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."),
145 'loc_barcode': fields.char('Location Barcode'),
150 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
154 'scrap_location': False,
156 _sql_constraints = [('loc_barcode_company_uniq', 'unique (loc_barcode,company_id)', 'The barcode for a location must be unique per company !')]
158 def create(self, cr, uid, default, context=None):
159 if not default.get('loc_barcode', False):
160 default.update({'loc_barcode': default.get('complete_name', False)})
161 return super(stock_location, self).create(cr, uid, default, context=context)
163 def get_putaway_strategy(self, cr, uid, location, product, context=None):
164 ''' Returns the location where the product has to be put, if any compliant putaway strategy is found. Otherwise returns None.'''
165 putaway_obj = self.pool.get('product.putaway')
168 if loc.putaway_strategy_id:
169 res = putaway_obj.putaway_apply(cr, uid, loc.putaway_strategy_id, product, context=context)
172 loc = loc.location_id
174 def _default_removal_strategy(self, cr, uid, context=None):
177 def get_removal_strategy(self, cr, uid, location, product, context=None):
178 ''' Returns the removal strategy to consider for the given product and location.
179 :param location: browse record (stock.location)
180 :param product: browse record (product.product)
183 if product.categ_id.removal_strategy_id:
184 return product.categ_id.removal_strategy_id.method
187 if loc.removal_strategy_id:
188 return loc.removal_strategy_id.method
189 loc = loc.location_id
190 return self._default_removal_strategy(cr, uid, context=context)
193 def get_warehouse(self, cr, uid, location, context=None):
195 Returns warehouse id of warehouse that contains location
196 :param location: browse record (stock.location)
198 wh_obj = self.pool.get("stock.warehouse")
199 whs = wh_obj.search(cr, uid, [('view_location_id.parent_left', '<=', location.parent_left),
200 ('view_location_id.parent_right', '>=', location.parent_left)], context=context)
201 return whs and whs[0] or False
203 #----------------------------------------------------------
205 #----------------------------------------------------------
207 class stock_location_route(osv.osv):
208 _name = 'stock.location.route'
209 _description = "Inventory Routes"
213 'name': fields.char('Route Name', required=True),
214 'sequence': fields.integer('Sequence'),
215 'pull_ids': fields.one2many('procurement.rule', 'route_id', 'Pull Rules', copy=True),
216 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it."),
217 'push_ids': fields.one2many('stock.location.path', 'route_id', 'Push Rules', copy=True),
218 'product_selectable': fields.boolean('Applicable on Product'),
219 'product_categ_selectable': fields.boolean('Applicable on Product Category'),
220 'warehouse_selectable': fields.boolean('Applicable on Warehouse'),
221 'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'),
222 'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'),
223 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this route is shared between all companies'),
227 'sequence': lambda self, cr, uid, ctx: 0,
229 'product_selectable': True,
230 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location.route', context=c),
233 def write(self, cr, uid, ids, vals, context=None):
234 '''when a route is deactivated, deactivate also its pull and push rules'''
235 if isinstance(ids, (int, long)):
237 res = super(stock_location_route, self).write(cr, uid, ids, vals, context=context)
241 for route in self.browse(cr, uid, ids, context=context):
243 push_ids += [r.id for r in route.push_ids if r.active != vals['active']]
245 pull_ids += [r.id for r in route.pull_ids if r.active != vals['active']]
247 self.pool.get('stock.location.path').write(cr, uid, push_ids, {'active': vals['active']}, context=context)
249 self.pool.get('procurement.rule').write(cr, uid, pull_ids, {'active': vals['active']}, context=context)
252 #----------------------------------------------------------
254 #----------------------------------------------------------
256 class stock_quant(osv.osv):
258 Quants are the smallest unit of stock physical instances
260 _name = "stock.quant"
261 _description = "Quants"
263 def _get_quant_name(self, cr, uid, ids, name, args, context=None):
264 """ Forms complete name of location from parent location to child location.
265 @return: Dictionary of values
268 for q in self.browse(cr, uid, ids, context=context):
270 res[q.id] = q.product_id.code or ''
272 res[q.id] = q.lot_id.name
273 res[q.id] += ': ' + str(q.qty) + q.product_id.uom_id.name
276 def _calc_inventory_value(self, cr, uid, ids, name, attr, context=None):
277 context = dict(context or {})
279 uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
280 for quant in self.browse(cr, uid, ids, context=context):
281 context.pop('force_company', None)
282 if quant.company_id.id != uid_company_id:
283 #if the company of the quant is different than the current user company, force the company in the context
284 #then re-do a browse to read the property fields for the good company.
285 context['force_company'] = quant.company_id.id
286 quant = self.browse(cr, uid, quant.id, context=context)
287 res[quant.id] = self._get_inventory_value(cr, uid, quant, context=context)
290 def _get_inventory_value(self, cr, uid, quant, context=None):
291 return quant.product_id.standard_price * quant.qty
294 'name': fields.function(_get_quant_name, type='char', string='Identifier'),
295 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete="restrict", readonly=True, select=True),
296 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="restrict", readonly=True, select=True),
297 'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product", readonly=True, select=True),
298 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant", readonly=True, select=True),
299 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', readonly=True, store=True),
300 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for", readonly=True, select=True),
301 'lot_id': fields.many2one('stock.production.lot', 'Lot', readonly=True, select=True),
302 'cost': fields.float('Unit Cost'),
303 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant", readonly=True, select=True),
305 'create_date': fields.datetime('Creation Date', readonly=True),
306 'in_date': fields.datetime('Incoming Date', readonly=True, select=True),
308 'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
309 'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True, readonly=True, select=True),
310 'inventory_value': fields.function(_calc_inventory_value, string="Inventory Value", type='float', readonly=True),
312 # Used for negative quants to reconcile after compensated by a new positive one
313 'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from', readonly=True, select=True),
314 'negative_move_id': fields.many2one('stock.move', 'Move Negative Quant', help='If this is a negative quant, this will be the move that caused this negative quant.', readonly=True),
315 'negative_dest_location_id': fields.related('negative_move_id', 'location_dest_id', type='many2one', relation='stock.location', string="Negative Destination Location", readonly=True,
316 help="Technical field used to record the destination location of a move that created a negative quant"),
320 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.quant', context=c),
323 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
324 ''' Overwrite the read_group in order to sum the function field 'inventory_value' in group by'''
325 res = super(stock_quant, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
326 if 'inventory_value' in fields:
328 if '__domain' in line:
329 lines = self.search(cr, uid, line['__domain'], context=context)
331 for line2 in self.browse(cr, uid, lines, context=context):
332 inv_value += line2.inventory_value
333 line['inventory_value'] = inv_value
336 def action_view_quant_history(self, cr, uid, ids, context=None):
338 This function returns an action that display the history of the quant, which
339 mean all the stock moves that lead to this quant creation with this quant quantity.
341 mod_obj = self.pool.get('ir.model.data')
342 act_obj = self.pool.get('ir.actions.act_window')
344 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_move_form2')
345 id = result and result[1] or False
346 result = act_obj.read(cr, uid, [id], context={})[0]
349 for quant in self.browse(cr, uid, ids, context=context):
350 move_ids += [move.id for move in quant.history_ids]
352 result['domain'] = "[('id','in',[" + ','.join(map(str, move_ids)) + "])]"
355 def quants_reserve(self, cr, uid, quants, move, link=False, context=None):
356 '''This function reserves quants for the given move (and optionally given link). If the total of quantity reserved is enough, the move's state
357 is also set to 'assigned'
359 :param quants: list of tuple(quant browse record or None, qty to reserve). If None is given as first tuple element, the item will be ignored. Negative quants should not be received as argument
360 :param move: browse record
361 :param link: browse record (stock.move.operation.link)
364 reserved_availability = move.reserved_availability
365 #split quants if needed
366 for quant, qty in quants:
367 if qty <= 0.0 or (quant and quant.qty <= 0.0):
368 raise osv.except_osv(_('Error!'), _('You can not reserve a negative quantity or a negative quant.'))
371 self._quant_split(cr, uid, quant, qty, context=context)
372 toreserve.append(quant.id)
373 reserved_availability += quant.qty
376 self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id}, context=context)
377 #if move has a picking_id, write on that picking that pack_operation might have changed and need to be recomputed
379 self.pool.get('stock.picking').write(cr, uid, [move.picking_id.id], {'recompute_pack_op': True}, context=context)
380 #check if move'state needs to be set as 'assigned'
381 rounding = move.product_id.uom_id.rounding
382 if float_compare(reserved_availability, move.product_qty, precision_rounding=rounding) == 0 and move.state in ('confirmed', 'waiting') :
383 self.pool.get('stock.move').write(cr, uid, [move.id], {'state': 'assigned'}, context=context)
384 elif float_compare(reserved_availability, 0, precision_rounding=rounding) > 0 and not move.partially_available:
385 self.pool.get('stock.move').write(cr, uid, [move.id], {'partially_available': True}, context=context)
387 def quants_move(self, cr, uid, quants, move, location_to, location_from=False, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, context=None):
388 """Moves all given stock.quant in the given destination location. Unreserve from current move.
389 :param quants: list of tuple(browse record(stock.quant) or None, quantity to move)
390 :param move: browse record (stock.move)
391 :param location_to: browse record (stock.location) depicting where the quants have to be moved
392 :param location_from: optional browse record (stock.location) explaining where the quant has to be taken (may differ from the move source location in case a removal strategy applied). This parameter is only used to pass to _quant_create if a negative quant must be created
393 :param lot_id: ID of the lot that must be set on the quants to move
394 :param owner_id: ID of the partner that must own the quants to move
395 :param src_package_id: ID of the package that contains the quants to move
396 :param dest_package_id: ID of the package that must be set on the moved quant
398 quants_reconcile = []
400 self._check_location(cr, uid, location_to, context=context)
401 for quant, qty in quants:
403 #If quant is None, we will create a quant to move (and potentially a negative counterpart too)
404 quant = self._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to, context=context)
406 self._quant_split(cr, uid, quant, qty, context=context)
407 to_move_quants.append(quant)
408 quants_reconcile.append(quant)
410 to_recompute_move_ids = [x.reservation_id.id for x in to_move_quants if x.reservation_id and x.reservation_id.id != move.id]
411 self.move_quants_write(cr, uid, to_move_quants, move, location_to, dest_package_id, context=context)
412 self.pool.get('stock.move').recalculate_move_state(cr, uid, to_recompute_move_ids, context=context)
413 if location_to.usage == 'internal':
414 if self.search(cr, uid, [('product_id', '=', move.product_id.id), ('qty','<', 0)], limit=1, context=context):
415 for quant in quants_reconcile:
416 self._quant_reconcile_negative(cr, uid, quant, move, context=context)
418 def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
419 vals = {'location_id': location_dest_id.id,
420 'history_ids': [(4, move.id)],
421 'package_id': dest_package_id,
422 'reservation_id': False}
423 self.write(cr, SUPERUSER_ID, [q.id for q in quants], vals, context=context)
425 def quants_get_prefered_domain(self, cr, uid, location, product, qty, domain=None, prefered_domain_list=[], restrict_lot_id=False, restrict_partner_id=False, context=None):
426 ''' This function tries to find quants in the given location for the given domain, by trying to first limit
427 the choice on the quants that match the first item of prefered_domain_list as well. But if the qty requested is not reached
428 it tries to find the remaining quantity by looping on the prefered_domain_list (tries with the second item and so on).
429 Make sure the quants aren't found twice => all the domains of prefered_domain_list should be orthogonal
433 quants = [(None, qty)]
434 #don't look for quants in location that are of type production, supplier or inventory.
435 if location.usage in ['inventory', 'production', 'supplier']:
438 if not prefered_domain_list:
439 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)
440 for prefered_domain in prefered_domain_list:
441 res_qty_cmp = float_compare(res_qty, 0, precision_rounding=product.uom_id.rounding)
443 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
445 tmp_quants = self.quants_get(cr, uid, location, product, res_qty, domain=domain + prefered_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
446 for quant in tmp_quants:
452 def quants_get(self, cr, uid, location, product, qty, domain=None, restrict_lot_id=False, restrict_partner_id=False, context=None):
454 Use the removal strategies of product to search for the correct quants
455 If you inherit, put the super at the end of your method.
457 :location: browse record of the parent location where the quants have to be found
458 :product: browse record of the product to find
459 :qty in UoM of product
462 domain = domain or [('qty', '>', 0.0)]
463 if restrict_partner_id:
464 domain += [('owner_id', '=', restrict_partner_id)]
466 domain += [('lot_id', '=', restrict_lot_id)]
468 removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context)
469 result += self.apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context)
472 def apply_removal_strategy(self, cr, uid, location, product, quantity, domain, removal_strategy, context=None):
473 if removal_strategy == 'fifo':
474 order = 'in_date, id'
475 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
476 elif removal_strategy == 'lifo':
477 order = 'in_date desc, id desc'
478 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
479 raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,)))
481 def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False,
482 force_location_from=False, force_location_to=False, context=None):
483 '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location.
487 price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
488 location = force_location_to or move.location_dest_id
489 rounding = move.product_id.uom_id.rounding
491 'product_id': move.product_id.id,
492 'location_id': location.id,
493 'qty': float_round(qty, precision_rounding=rounding),
495 'history_ids': [(4, move.id)],
496 'in_date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
497 'company_id': move.company_id.id,
499 'owner_id': owner_id,
500 'package_id': dest_package_id,
503 if move.location_id.usage == 'internal':
504 #if we were trying to move something from an internal location and reach here (quant creation),
505 #it means that a negative quant has to be created as well.
506 negative_vals = vals.copy()
507 negative_vals['location_id'] = force_location_from and force_location_from.id or move.location_id.id
508 negative_vals['qty'] = float_round(-qty, precision_rounding=rounding)
509 negative_vals['cost'] = price_unit
510 negative_vals['negative_move_id'] = move.id
511 negative_vals['package_id'] = src_package_id
512 negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
513 vals.update({'propagated_from_id': negative_quant_id})
515 #create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
516 quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
517 return self.browse(cr, uid, quant_id, context=context)
519 def _quant_split(self, cr, uid, quant, qty, context=None):
520 context = context or {}
521 rounding = quant.product_id.uom_id.rounding
522 if float_compare(abs(quant.qty), abs(qty), precision_rounding=rounding) <= 0: # if quant <= qty in abs, take it entirely
524 qty_round = float_round(qty, precision_rounding=rounding)
525 new_qty_round = float_round(quant.qty - qty, precision_rounding=rounding)
526 new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': new_qty_round, 'history_ids': [(4, x.id) for x in quant.history_ids]}, context=context)
527 self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty_round}, context=context)
528 return self.browse(cr, uid, new_quant, context=context)
530 def _get_latest_move(self, cr, uid, quant, context=None):
532 for m in quant.history_ids:
533 if not move or m.date > move.date:
537 @api.cr_uid_ids_context
538 def _quants_merge(self, cr, uid, solved_quant_ids, solving_quant, context=None):
540 for move in solving_quant.history_ids:
541 path.append((4, move.id))
542 self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
544 def _quant_reconcile_negative(self, cr, uid, quant, move, context=None):
546 When new quant arrive in a location, try to reconcile it with
547 negative quants. If it's possible, apply the cost of the new
548 quant to the conter-part of the negative quant.
550 solving_quant = quant
551 dom = [('qty', '<', 0)]
553 dom += [('lot_id', '=', quant.lot_id.id)]
554 dom += [('owner_id', '=', quant.owner_id.id)]
555 dom += [('package_id', '=', quant.package_id.id)]
556 dom += [('id', '!=', quant.propagated_from_id.id)]
557 quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, dom, context=context)
558 product_uom_rounding = quant.product_id.uom_id.rounding
559 for quant_neg, qty in quants:
560 if not quant_neg or not solving_quant:
562 to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
563 if not to_solve_quant_ids:
566 solved_quant_ids = []
567 for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
568 if float_compare(solving_qty, 0, precision_rounding=product_uom_rounding) <= 0:
570 solved_quant_ids.append(to_solve_quant.id)
571 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
572 solving_qty -= min(solving_qty, to_solve_quant.qty)
573 remaining_solving_quant = self._quant_split(cr, uid, solving_quant, qty, context=context)
574 remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
575 #if the reconciliation was not complete, we need to link together the remaining parts
576 if remaining_neg_quant:
577 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
578 if remaining_to_solve_quant_ids:
579 self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
580 if solving_quant.propagated_from_id and solved_quant_ids:
581 self.write(cr, uid, solved_quant_ids, {'propagated_from_id': solving_quant.propagated_from_id.id}, context=context)
582 #delete the reconciled quants, as it is replaced by the solved quants
583 self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
585 #price update + accounting entries adjustments
586 self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
587 #merge history (and cost?)
588 self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
589 self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
590 solving_quant = remaining_solving_quant
592 def _price_update(self, cr, uid, ids, newprice, context=None):
593 self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context)
595 def quants_unreserve(self, cr, uid, move, context=None):
596 related_quants = [x.id for x in move.reserved_quant_ids]
598 #if move has a picking_id, write on that picking that pack_operation might have changed and need to be recomputed
600 self.pool.get('stock.picking').write(cr, uid, [move.picking_id.id], {'recompute_pack_op': True}, context=context)
601 if move.partially_available:
602 self.pool.get("stock.move").write(cr, uid, [move.id], {'partially_available': False}, context=context)
603 self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False}, context=context)
605 def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
606 ''' Implementation of removal strategies
607 If it can not reserve, it will return a tuple (None, qty)
611 domain += location and [('location_id', 'child_of', location.id)] or []
612 domain += [('product_id', '=', product.id)]
613 if context.get('force_company'):
614 domain += [('company_id', '=', context.get('force_company'))]
616 domain += [('company_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)]
619 while float_compare(quantity, 0, precision_rounding=product.uom_id.rounding) > 0:
620 quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
622 res.append((None, quantity))
624 for quant in self.browse(cr, uid, quants, context=context):
625 rounding = product.uom_id.rounding
626 if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0:
627 res += [(quant, abs(quant.qty))]
628 quantity -= abs(quant.qty)
629 elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0:
630 res += [(quant, quantity)]
636 def _check_location(self, cr, uid, location, context=None):
637 if location.usage == 'view':
638 raise osv.except_osv(_('Error'), _('You cannot move to a location of type view %s.') % (location.name))
641 #----------------------------------------------------------
643 #----------------------------------------------------------
645 class stock_picking(osv.osv):
646 _name = "stock.picking"
647 _inherit = ['mail.thread']
648 _description = "Transfer"
649 _order = "priority desc, date asc, id desc"
651 def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
652 move_obj = self.pool.get("stock.move")
654 move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
655 move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
657 def _set_priority(self, cr, uid, id, field, value, arg, context=None):
658 move_obj = self.pool.get("stock.move")
660 move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
661 move_obj.write(cr, uid, move_ids, {'priority': value}, context=context)
663 def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
664 """ Finds minimum and maximum dates for picking.
665 @return: Dictionary of values
669 res[id] = {'min_date': False, 'max_date': False, 'priority': '1'}
682 picking_id""", (tuple(ids),))
683 for pick, dt1, dt2, prio in cr.fetchall():
684 res[pick]['min_date'] = dt1
685 res[pick]['max_date'] = dt2
686 res[pick]['priority'] = prio
689 def create(self, cr, user, vals, context=None):
690 context = context or {}
691 if ('name' not in vals) or (vals.get('name') in ('/', False)):
692 ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
693 sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
694 vals['name'] = self.pool.get('ir.sequence').next_by_id(cr, user, sequence_id, context=context)
695 return super(stock_picking, self).create(cr, user, vals, context)
697 def _state_get(self, cr, uid, ids, field_name, arg, context=None):
698 '''The state of a picking depends on the state of its related stock.move
699 draft: the picking has no line or any one of the lines is draft
700 done, draft, cancel: all lines are done / draft / cancel
701 confirmed, waiting, assigned, partially_available depends on move_type (all at once or partial)
704 for pick in self.browse(cr, uid, ids, context=context):
705 if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]):
706 res[pick.id] = 'draft'
708 if all([x.state == 'cancel' for x in pick.move_lines]):
709 res[pick.id] = 'cancel'
711 if all([x.state in ('cancel', 'done') for x in pick.move_lines]):
712 res[pick.id] = 'done'
715 order = {'confirmed': 0, 'waiting': 1, 'assigned': 2}
716 order_inv = {0: 'confirmed', 1: 'waiting', 2: 'assigned'}
717 lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel', 'done')]
718 if pick.move_type == 'one':
719 res[pick.id] = order_inv[min(lst)]
721 #we are in the case of partial delivery, so if all move are assigned, picking
722 #should be assign too, else if one of the move is assigned, or partially available, picking should be
723 #in partially available state, otherwise, picking is in waiting or confirmed state
724 res[pick.id] = order_inv[max(lst)]
725 if not all(x == 2 for x in lst):
726 if any(x == 2 for x in lst):
727 res[pick.id] = 'partially_available'
729 #if all moves aren't assigned, check if we have one product partially available
730 for move in pick.move_lines:
731 if move.partially_available:
732 res[pick.id] = 'partially_available'
736 def _get_pickings(self, cr, uid, ids, context=None):
738 for move in self.browse(cr, uid, ids, context=context):
740 res.add(move.picking_id.id)
743 def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
745 for pick in self.browse(cr, uid, ids, context=context):
747 if pick.pack_operation_ids:
751 def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
753 for pick in self.browse(cr, uid, ids, context=context):
755 for move in pick.move_lines:
756 if move.reserved_quant_ids:
761 def check_group_lot(self, cr, uid, context=None):
762 """ This function will return true if we have the setting to use lots activated. """
763 return self.pool.get('res.users').has_group(cr, uid, 'stock.group_production_lot')
765 def check_group_pack(self, cr, uid, context=None):
766 """ This function will return true if we have the setting to use package activated. """
767 return self.pool.get('res.users').has_group(cr, uid, 'stock.group_tracking_lot')
769 def action_assign_owner(self, cr, uid, ids, context=None):
770 for picking in self.browse(cr, uid, ids, context=context):
771 packop_ids = [op.id for op in picking.pack_operation_ids]
772 self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
775 'name': fields.char('Reference', select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=False),
776 'origin': fields.char('Source Document', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document", select=True),
777 '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, copy=False),
778 'note': fields.text('Notes', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
779 '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"),
780 'state': fields.function(_state_get, type="selection", copy=False,
782 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type'], 20),
783 'stock.move': (_get_pickings, ['state', 'picking_id', 'partially_available'], 20)},
786 ('cancel', 'Cancelled'),
787 ('waiting', 'Waiting Another Operation'),
788 ('confirmed', 'Waiting Availability'),
789 ('partially_available', 'Partially Available'),
790 ('assigned', 'Ready to Transfer'),
791 ('done', 'Transferred'),
792 ], string='Status', readonly=True, select=True, track_visibility='onchange',
794 * Draft: not confirmed yet and will not be scheduled until confirmed\n
795 * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
796 * Waiting Availability: still waiting for the availability of products\n
797 * Partially Available: some products are available and reserved\n
798 * Ready to Transfer: products reserved, simply waiting for confirmation.\n
799 * Transferred: has been processed, can't be modified or cancelled anymore\n
800 * Cancelled: has been cancelled, can't be confirmed anymore"""
802 'priority': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_priority, type='selection', selection=procurement.PROCUREMENT_PRIORITIES, string='Priority',
803 store={'stock.move': (_get_pickings, ['priority', 'picking_id'], 20)}, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, select=1, help="Priority for this picking. Setting manually a value here would set it as priority for all the moves",
804 track_visibility='onchange', required=True),
805 'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
806 store={'stock.move': (_get_pickings, ['date_expected', 'picking_id'], 20)}, type='datetime', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.", track_visibility='onchange'),
807 'max_date': fields.function(get_min_max_date, multi="min_max_date",
808 store={'stock.move': (_get_pickings, ['date_expected', 'picking_id'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
809 'date': fields.datetime('Creation Date', help="Creation Date, usually the time of the order", select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, track_visibility='onchange'),
810 'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=False),
811 'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=True),
812 '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'),
813 'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
814 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
815 'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Related Packing Operations'),
816 'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
817 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, required=True),
818 'picking_type_code': fields.related('picking_type_id', 'code', type='char', string='Picking Type Code', help="Technical field used to display the correct label on print button in the picking view"),
820 'owner_id': fields.many2one('res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner"),
821 # Used to search on pickings
822 'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),
823 'recompute_pack_op': fields.boolean('Recompute pack operation?', help='True if reserved quants changed, which mean we might need to recompute the package operations', copy=False),
824 'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
825 'location_dest_id': fields.related('move_lines', 'location_dest_id', type='many2one', relation='stock.location', string='Destination Location', readonly=True),
826 'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True,
828 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
829 'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
836 'move_type': 'direct',
837 'priority': '1', # normal
838 'date': fields.datetime.now,
839 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c),
840 'recompute_pack_op': True,
843 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
846 def do_print_picking(self, cr, uid, ids, context=None):
847 '''This function prints the picking list'''
848 context = dict(context or {}, active_ids=ids)
849 return self.pool.get("report").get_action(cr, uid, ids, 'stock.report_picking', context=context)
852 def action_confirm(self, cr, uid, ids, context=None):
854 todo_force_assign = []
855 for picking in self.browse(cr, uid, ids, context=context):
856 if picking.location_id.usage in ('supplier', 'inventory', 'production'):
857 todo_force_assign.append(picking.id)
858 for r in picking.move_lines:
859 if r.state == 'draft':
862 self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
864 if todo_force_assign:
865 self.force_assign(cr, uid, todo_force_assign, context=context)
868 def action_assign(self, cr, uid, ids, context=None):
869 """ Check availability of picking moves.
870 This has the effect of changing the state and reserve quants on available moves, and may
871 also impact the state of the picking as it is computed based on move's states.
874 for pick in self.browse(cr, uid, ids, context=context):
875 if pick.state == 'draft':
876 self.action_confirm(cr, uid, [pick.id], context=context)
877 #skip the moves that don't need to be checked
878 move_ids = [x.id for x in pick.move_lines if x.state not in ('draft', 'cancel', 'done')]
880 raise osv.except_osv(_('Warning!'), _('Nothing to check the availability for.'))
881 self.pool.get('stock.move').action_assign(cr, uid, move_ids, context=context)
884 def force_assign(self, cr, uid, ids, context=None):
885 """ Changes state of picking to available if moves are confirmed or waiting.
888 for pick in self.browse(cr, uid, ids, context=context):
889 move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
890 self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
891 #pack_operation might have changed and need to be recomputed
892 self.write(cr, uid, ids, {'recompute_pack_op': True}, context=context)
895 def action_cancel(self, cr, uid, ids, context=None):
896 for pick in self.browse(cr, uid, ids, context=context):
897 ids2 = [move.id for move in pick.move_lines]
898 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
901 def action_done(self, cr, uid, ids, context=None):
902 """Changes picking state to done by processing the Stock Moves of the Picking
904 Normally that happens when the button "Done" is pressed on a Picking view.
907 for pick in self.browse(cr, uid, ids, context=context):
909 for move in pick.move_lines:
910 if move.state == 'draft':
911 todo.extend(self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context))
912 elif move.state in ('assigned', 'confirmed'):
915 self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
918 def unlink(self, cr, uid, ids, context=None):
919 #on picking deletion, cancel its move then unlink them too
920 move_obj = self.pool.get('stock.move')
921 context = context or {}
922 for pick in self.browse(cr, uid, ids, context=context):
923 move_ids = [move.id for move in pick.move_lines]
924 move_obj.action_cancel(cr, uid, move_ids, context=context)
925 move_obj.unlink(cr, uid, move_ids, context=context)
926 return super(stock_picking, self).unlink(cr, uid, ids, context=context)
928 def write(self, cr, uid, ids, vals, context=None):
929 res = super(stock_picking, self).write(cr, uid, ids, vals, context=context)
930 #if we changed the move lines or the pack operations, we need to recompute the remaining quantities of both
931 if 'move_lines' in vals or 'pack_operation_ids' in vals:
932 self.do_recompute_remaining_quantities(cr, uid, ids, context=context)
935 def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
936 """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
938 if not backorder_moves:
939 backorder_moves = picking.move_lines
940 backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done', 'cancel')]
941 if 'do_only_split' in context and context['do_only_split']:
942 backorder_move_ids = [x.id for x in backorder_moves if x.id not in context.get('split', [])]
944 if backorder_move_ids:
945 backorder_id = self.copy(cr, uid, picking.id, {
948 'pack_operation_ids': [],
949 'backorder_id': picking.id,
951 backorder = self.browse(cr, uid, backorder_id, context=context)
952 self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (backorder.name), context=context)
953 move_obj = self.pool.get("stock.move")
954 move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
956 self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
957 self.action_confirm(cr, uid, [backorder_id], context=context)
961 @api.cr_uid_ids_context
962 def recheck_availability(self, cr, uid, picking_ids, context=None):
963 self.action_assign(cr, uid, picking_ids, context=context)
964 self.do_prepare_partial(cr, uid, picking_ids, context=context)
966 def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None):
967 """This method searches for the higher level packages that can be moved as a single operation, given a list of quants
968 to move and their suggested destination, and returns the list of matching packages.
970 # Try to find as much as possible top-level packages that can be moved
971 pack_obj = self.pool.get("stock.quant.package")
972 quant_obj = self.pool.get("stock.quant")
973 top_lvl_packages = set()
974 quants_to_compare = quants_suggested_locations.keys()
975 for pack in list(set([x.package_id for x in quants_suggested_locations.keys() if x and x.package_id])):
979 pack_destination = False
981 pack_quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
983 for quant in quant_obj.browse(cr, uid, pack_quants, context=context):
984 # If the quant is not in the quants to compare and not in the common location
985 if not quant in quants_to_compare:
989 #if putaway strat apply, the destination location of each quant may be different (and thus the package should not be taken as a single operation)
990 if not pack_destination:
991 pack_destination = quants_suggested_locations[quant]
992 elif pack_destination != quants_suggested_locations[quant]:
996 good_pack = test_pack
997 if test_pack.parent_id:
998 test_pack = test_pack.parent_id
1000 #stop the loop when there's no parent package anymore
1003 #stop the loop when the package test_pack is not totally reserved for moves of this picking
1004 #(some quants may be reserved for other picking or not reserved at all)
1007 top_lvl_packages.add(good_pack)
1008 return list(top_lvl_packages)
1010 def _prepare_pack_ops(self, cr, uid, picking, quants, forced_qties, context=None):
1011 """ returns a list of dict, ready to be used in create() of stock.pack.operation.
1013 :param picking: browse record (stock.picking)
1014 :param quants: browse record list (stock.quant). List of quants associated to the picking
1015 :param forced_qties: dictionary showing for each product (keys) its corresponding quantity (value) that is not covered by the quants associated to the picking
1017 def _picking_putaway_apply(product):
1019 # Search putaway strategy
1020 if product_putaway_strats.get(product.id):
1021 location = product_putaway_strats[product.id]
1023 location = self.pool.get('stock.location').get_putaway_strategy(cr, uid, picking.location_dest_id, product, context=context)
1024 product_putaway_strats[product.id] = location
1025 return location or picking.location_dest_id.id
1027 # If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
1028 product_uom = {} # Determines UoM used in pack operations
1029 for move in picking.move_lines:
1030 if not product_uom.get(move.product_id.id):
1031 product_uom[move.product_id.id] = move.product_id.uom_id.id
1032 if move.product_uom.id != move.product_id.uom_id.id and move.product_uom.factor > product_uom[move.product_id.id]:
1033 product_uom[move.product_id.id] = move.product_uom.id
1035 pack_obj = self.pool.get("stock.quant.package")
1036 quant_obj = self.pool.get("stock.quant")
1039 #for each quant of the picking, find the suggested location
1040 quants_suggested_locations = {}
1041 product_putaway_strats = {}
1042 for quant in quants:
1045 suggested_location_id = _picking_putaway_apply(quant.product_id)
1046 quants_suggested_locations[quant] = suggested_location_id
1048 #find the packages we can movei as a whole
1049 top_lvl_packages = self._get_top_level_packages(cr, uid, quants_suggested_locations, context=context)
1050 # and then create pack operations for the top-level packages found
1051 for pack in top_lvl_packages:
1052 pack_quant_ids = pack_obj.get_content(cr, uid, [pack.id], context=context)
1053 pack_quants = quant_obj.browse(cr, uid, pack_quant_ids, context=context)
1055 'picking_id': picking.id,
1056 'package_id': pack.id,
1058 'location_id': pack.location_id.id,
1059 'location_dest_id': quants_suggested_locations[pack_quants[0]],
1061 #remove the quants inside the package so that they are excluded from the rest of the computation
1062 for quant in pack_quants:
1063 del quants_suggested_locations[quant]
1065 # Go through all remaining reserved quants and group by product, package, lot, owner, source location and dest location
1066 for quant, dest_location_id in quants_suggested_locations.items():
1067 key = (quant.product_id.id, quant.package_id.id, quant.lot_id.id, quant.owner_id.id, quant.location_id.id, dest_location_id)
1068 if qtys_grouped.get(key):
1069 qtys_grouped[key] += quant.qty
1071 qtys_grouped[key] = quant.qty
1073 # Do the same for the forced quantities (in cases of force_assign or incomming shipment for example)
1074 for product, qty in forced_qties.items():
1077 suggested_location_id = _picking_putaway_apply(product)
1078 key = (product.id, False, False, False, picking.location_id.id, suggested_location_id)
1079 if qtys_grouped.get(key):
1080 qtys_grouped[key] += qty
1082 qtys_grouped[key] = qty
1084 # Create the necessary operations for the grouped quants and remaining qtys
1085 uom_obj = self.pool.get('product.uom')
1086 for key, qty in qtys_grouped.items():
1087 product = self.pool.get("product.product").browse(cr, uid, key[0], context=context)
1088 uom_id = product.uom_id.id
1090 if product_uom.get(key[0]):
1091 uom_id = product_uom[key[0]]
1092 qty_uom = uom_obj._compute_qty(cr, uid, product.uom_id.id, qty, uom_id)
1094 'picking_id': picking.id,
1095 'product_qty': qty_uom,
1096 'product_id': key[0],
1097 'package_id': key[1],
1100 'location_id': key[4],
1101 'location_dest_id': key[5],
1102 'product_uom_id': uom_id,
1106 @api.cr_uid_ids_context
1107 def open_barcode_interface(self, cr, uid, picking_ids, context=None):
1108 final_url="/stock/barcode/#action=stock.ui&picking_id="+str(picking_ids[0])
1109 return {'type': 'ir.actions.act_url', 'url':final_url, 'target': 'self',}
1111 @api.cr_uid_ids_context
1112 def do_partial_open_barcode(self, cr, uid, picking_ids, context=None):
1113 self.do_prepare_partial(cr, uid, picking_ids, context=context)
1114 return self.open_barcode_interface(cr, uid, picking_ids, context=context)
1116 @api.cr_uid_ids_context
1117 def do_prepare_partial(self, cr, uid, picking_ids, context=None):
1118 context = context or {}
1119 pack_operation_obj = self.pool.get('stock.pack.operation')
1120 #used to avoid recomputing the remaining quantities at each new pack operation created
1121 ctx = context.copy()
1122 ctx['no_recompute'] = True
1124 #get list of existing operations and delete them
1125 existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context)
1126 if existing_package_ids:
1127 pack_operation_obj.unlink(cr, uid, existing_package_ids, context)
1128 for picking in self.browse(cr, uid, picking_ids, context=context):
1129 forced_qties = {} # Quantity remaining after calculating reserved quants
1131 #Calculate packages, reserved quants, qtys of this picking's moves
1132 for move in picking.move_lines:
1133 if move.state not in ('assigned', 'confirmed'):
1135 move_quants = move.reserved_quant_ids
1136 picking_quants += move_quants
1137 forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0
1138 #if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
1139 if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
1140 if forced_qties.get(move.product_id):
1141 forced_qties[move.product_id] += forced_qty
1143 forced_qties[move.product_id] = forced_qty
1144 for vals in self._prepare_pack_ops(cr, uid, picking, picking_quants, forced_qties, context=context):
1145 pack_operation_obj.create(cr, uid, vals, context=ctx)
1146 #recompute the remaining quantities all at once
1147 self.do_recompute_remaining_quantities(cr, uid, picking_ids, context=context)
1148 self.write(cr, uid, picking_ids, {'recompute_pack_op': False}, context=context)
1150 @api.cr_uid_ids_context
1151 def do_unreserve(self, cr, uid, picking_ids, context=None):
1153 Will remove all quants for picking in picking_ids
1155 moves_to_unreserve = []
1156 pack_line_to_unreserve = []
1157 for picking in self.browse(cr, uid, picking_ids, context=context):
1158 moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')]
1159 pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids]
1160 if moves_to_unreserve:
1161 if pack_line_to_unreserve:
1162 self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
1163 self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context)
1165 def recompute_remaining_qty(self, cr, uid, picking, context=None):
1166 def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False):
1167 move_dict = prod2move_ids[product_id][index]
1168 qty_on_link = min(move_dict['remaining_qty'], qty_to_assign)
1169 self.pool.get('stock.move.operation.link').create(cr, uid, {'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id}, context=context)
1170 if move_dict['remaining_qty'] == qty_on_link:
1171 prod2move_ids[product_id].pop(index)
1173 move_dict['remaining_qty'] -= qty_on_link
1176 def _create_link_for_quant(operation_id, quant, qty):
1177 """create a link for given operation and reserved move of given quant, for the max quantity possible, and returns this quantity"""
1178 if not quant.reservation_id.id:
1179 return _create_link_for_product(operation_id, quant.product_id.id, qty)
1181 for i in range(0, len(prod2move_ids[quant.product_id.id])):
1182 if prod2move_ids[quant.product_id.id][i]['move'].id != quant.reservation_id.id:
1184 qty_on_link = _create_link_for_index(operation_id, i, quant.product_id.id, qty, quant_id=quant.id)
1188 def _create_link_for_product(operation_id, product_id, qty):
1189 '''method that creates the link between a given operation and move(s) of given product, for the given quantity.
1190 Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
1192 prod_obj = self.pool.get("product.product")
1193 product = prod_obj.browse(cr, uid, product_id)
1194 rounding = product.uom_id.rounding
1195 qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
1196 if prod2move_ids.get(product_id):
1197 while prod2move_ids[product_id] and qtyassign_cmp > 0:
1198 qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False)
1199 qty_to_assign -= qty_on_link
1200 qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
1201 return qtyassign_cmp == 0
1203 uom_obj = self.pool.get('product.uom')
1204 package_obj = self.pool.get('stock.quant.package')
1205 quant_obj = self.pool.get('stock.quant')
1206 link_obj = self.pool.get('stock.move.operation.link')
1207 quants_in_package_done = set()
1210 #make a dictionary giving for each product, the moves and related quantity that can be used in operation links
1211 for move in picking.move_lines:
1212 if not prod2move_ids.get(move.product_id.id):
1213 prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}]
1215 prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty})
1217 need_rereserve = False
1218 #sort the operations in order to give higher priority to those with a package, then a serial number
1219 operations = picking.pack_operation_ids
1220 operations = sorted(operations, key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
1221 #delete existing operations to start again from scratch
1222 links = link_obj.search(cr, uid, [('operation_id', 'in', [x.id for x in operations])], context=context)
1224 link_obj.unlink(cr, uid, links, context=context)
1225 #1) first, try to create links when quants can be identified without any doubt
1226 for ops in operations:
1227 #for each operation, create the links with the stock move by seeking on the matching reserved quants,
1228 #and deffer the operation if there is some ambiguity on the move to select
1229 if ops.package_id and not ops.product_id:
1231 quant_ids = package_obj.get_content(cr, uid, [ops.package_id.id], context=context)
1232 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1233 remaining_qty_on_quant = quant.qty
1234 if quant.reservation_id:
1235 #avoid quants being counted twice
1236 quants_in_package_done.add(quant.id)
1237 qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty)
1238 remaining_qty_on_quant -= qty_on_link
1239 if remaining_qty_on_quant:
1240 still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant))
1241 need_rereserve = True
1242 elif ops.product_id.id:
1243 #Check moves with same product
1244 qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
1245 for move_dict in prod2move_ids.get(ops.product_id.id, []):
1246 move = move_dict['move']
1247 for quant in move.reserved_quant_ids:
1248 if not qty_to_assign > 0:
1250 if quant.id in quants_in_package_done:
1253 #check if the quant is matching the operation details
1255 flag = quant.package_id and bool(package_obj.search(cr, uid, [('id', 'child_of', [ops.package_id.id])], context=context)) or False
1257 flag = not quant.package_id.id
1258 flag = flag and ((ops.lot_id and ops.lot_id.id == quant.lot_id.id) or not ops.lot_id)
1259 flag = flag and (ops.owner_id.id == quant.owner_id.id)
1261 max_qty_on_link = min(quant.qty, qty_to_assign)
1262 qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
1263 qty_to_assign -= qty_on_link
1264 qty_assign_cmp = float_compare(qty_to_assign, 0, precision_rounding=ops.product_id.uom_id.rounding)
1265 if qty_assign_cmp > 0:
1266 #qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
1267 #all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
1268 still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
1269 need_rereserve = True
1271 #2) then, process the remaining part
1272 all_op_processed = True
1273 for ops, product_id, remaining_qty in still_to_do:
1274 all_op_processed = all_op_processed and _create_link_for_product(ops.id, product_id, remaining_qty)
1275 return (need_rereserve, all_op_processed)
1277 def picking_recompute_remaining_quantities(self, cr, uid, picking, context=None):
1278 need_rereserve = False
1279 all_op_processed = True
1280 if picking.pack_operation_ids:
1281 need_rereserve, all_op_processed = self.recompute_remaining_qty(cr, uid, picking, context=context)
1282 return need_rereserve, all_op_processed
1284 @api.cr_uid_ids_context
1285 def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None):
1286 for picking in self.browse(cr, uid, picking_ids, context=context):
1287 if picking.pack_operation_ids:
1288 self.recompute_remaining_qty(cr, uid, picking, context=context)
1290 def _prepare_values_extra_move(self, cr, uid, op, product, remaining_qty, context=None):
1292 Creates an extra move when there is no corresponding original move to be copied
1294 uom_obj = self.pool.get("product.uom")
1295 uom_id = product.uom_id.id
1297 if op.product_id and op.product_uom_id and op.product_uom_id.id != product.uom_id.id:
1298 if op.product_uom_id.factor > product.uom_id.factor: #If the pack operation's is a smaller unit
1299 uom_id = op.product_uom_id.id
1300 #HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
1301 qty = uom_obj._compute_qty_obj(cr, uid, product.uom_id, remaining_qty, op.product_uom_id, rounding_method='HALF-UP')
1302 picking = op.picking_id
1304 'picking_id': picking.id,
1305 'location_id': picking.location_id.id,
1306 'location_dest_id': picking.location_dest_id.id,
1307 'product_id': product.id,
1308 'product_uom': uom_id,
1309 'product_uom_qty': qty,
1310 'name': _('Extra Move: ') + product.name,
1315 def _create_extra_moves(self, cr, uid, picking, context=None):
1316 '''This function creates move lines on a picking, at the time of do_transfer, based on
1317 unexpected product transfers (or exceeding quantities) found in the pack operations.
1319 move_obj = self.pool.get('stock.move')
1320 operation_obj = self.pool.get('stock.pack.operation')
1322 for op in picking.pack_operation_ids:
1323 for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items():
1324 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
1325 if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0:
1326 vals = self._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context)
1327 moves.append(move_obj.create(cr, uid, vals, context=context))
1329 move_obj.action_confirm(cr, uid, moves, context=context)
1332 def rereserve_pick(self, cr, uid, ids, context=None):
1334 This can be used to provide a button that rereserves taking into account the existing pack operations
1336 for pick in self.browse(cr, uid, ids, context=context):
1337 self.rereserve_quants(cr, uid, pick, move_ids = [x.id for x in pick.move_lines], context=context)
1339 def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
1340 """ Unreserve quants then try to reassign quants."""
1341 stock_move_obj = self.pool.get('stock.move')
1343 self.do_unreserve(cr, uid, [picking.id], context=context)
1344 self.action_assign(cr, uid, [picking.id], context=context)
1346 stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
1347 stock_move_obj.action_assign(cr, uid, move_ids, context=context)
1349 @api.cr_uid_ids_context
1350 def do_enter_transfer_details(self, cr, uid, picking, context=None):
1355 'active_model': self._name,
1356 'active_ids': picking,
1357 'active_id': len(picking) and picking[0] or False
1360 created_id = self.pool['stock.transfer_details'].create(cr, uid, {'picking_id': len(picking) and picking[0] or False}, context)
1361 return self.pool['stock.transfer_details'].wizard_view(cr, uid, created_id, context)
1364 @api.cr_uid_ids_context
1365 def do_transfer(self, cr, uid, picking_ids, context=None):
1367 If no pack operation, we do simple action_done of the picking
1368 Otherwise, do the pack operations
1372 stock_move_obj = self.pool.get('stock.move')
1373 for picking in self.browse(cr, uid, picking_ids, context=context):
1374 if not picking.pack_operation_ids:
1375 self.action_done(cr, uid, [picking.id], context=context)
1378 need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, picking, context=context)
1379 #create extra moves in the picking (unexpected product moves coming from pack operations)
1381 if not all_op_processed:
1382 todo_move_ids += self._create_extra_moves(cr, uid, picking, context=context)
1384 #split move lines if needed
1385 toassign_move_ids = []
1386 for move in picking.move_lines:
1387 remaining_qty = move.remaining_qty
1388 if move.state in ('done', 'cancel'):
1389 #ignore stock moves cancelled or already done
1391 elif move.state == 'draft':
1392 toassign_move_ids.append(move.id)
1393 if float_compare(remaining_qty, 0, precision_rounding = move.product_id.uom_id.rounding) == 0:
1394 if move.state in ('draft', 'assigned', 'confirmed'):
1395 todo_move_ids.append(move.id)
1396 elif float_compare(remaining_qty,0, precision_rounding = move.product_id.uom_id.rounding) > 0 and \
1397 float_compare(remaining_qty, move.product_qty, precision_rounding = move.product_id.uom_id.rounding) < 0:
1398 new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context)
1399 todo_move_ids.append(move.id)
1400 #Assign move as it was assigned before
1401 toassign_move_ids.append(new_move)
1402 if need_rereserve or not all_op_processed:
1403 if not picking.location_id.usage in ("supplier", "production", "inventory"):
1404 self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context)
1405 self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
1406 if todo_move_ids and not context.get('do_only_split'):
1407 self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context)
1408 elif context.get('do_only_split'):
1409 context = dict(context, split=todo_move_ids)
1410 self._create_backorder(cr, uid, picking, context=context)
1411 if toassign_move_ids:
1412 stock_move_obj.action_assign(cr, uid, toassign_move_ids, context=context)
1415 @api.cr_uid_ids_context
1416 def do_split(self, cr, uid, picking_ids, context=None):
1417 """ just split the picking (create a backorder) without making it 'done' """
1420 ctx = context.copy()
1421 ctx['do_only_split'] = True
1422 return self.do_transfer(cr, uid, picking_ids, context=ctx)
1424 def get_next_picking_for_ui(self, cr, uid, context=None):
1425 """ returns the next pickings to process. Used in the barcode scanner UI"""
1428 domain = [('state', 'in', ('assigned', 'partially_available'))]
1429 if context.get('default_picking_type_id'):
1430 domain.append(('picking_type_id', '=', context['default_picking_type_id']))
1431 return self.search(cr, uid, domain, context=context)
1433 def action_done_from_ui(self, cr, uid, picking_id, context=None):
1434 """ called when button 'done' is pushed in the barcode scanner UI """
1435 #write qty_done into field product_qty for every package_operation before doing the transfer
1436 pack_op_obj = self.pool.get('stock.pack.operation')
1437 for operation in self.browse(cr, uid, picking_id, context=context).pack_operation_ids:
1438 pack_op_obj.write(cr, uid, operation.id, {'product_qty': operation.qty_done}, context=context)
1439 self.do_transfer(cr, uid, [picking_id], context=context)
1440 #return id of next picking to work on
1441 return self.get_next_picking_for_ui(cr, uid, context=context)
1443 @api.cr_uid_ids_context
1444 def action_pack(self, cr, uid, picking_ids, operation_filter_ids=None, context=None):
1445 """ Create a package with the current pack_operation_ids of the picking that aren't yet in a pack.
1446 Used in the barcode scanner UI and the normal interface as well.
1447 operation_filter_ids is used by barcode scanner interface to specify a subset of operation to pack"""
1448 if operation_filter_ids == None:
1449 operation_filter_ids = []
1450 stock_operation_obj = self.pool.get('stock.pack.operation')
1451 package_obj = self.pool.get('stock.quant.package')
1452 stock_move_obj = self.pool.get('stock.move')
1453 for picking_id in picking_ids:
1454 operation_search_domain = [('picking_id', '=', picking_id), ('result_package_id', '=', False)]
1455 if operation_filter_ids != []:
1456 operation_search_domain.append(('id', 'in', operation_filter_ids))
1457 operation_ids = stock_operation_obj.search(cr, uid, operation_search_domain, context=context)
1458 pack_operation_ids = []
1460 for operation in stock_operation_obj.browse(cr, uid, operation_ids, context=context):
1461 #If we haven't done all qty in operation, we have to split into 2 operation
1463 if (operation.qty_done < operation.product_qty):
1464 new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context)
1465 stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0, 'lot_id': False}, context=context)
1466 op = stock_operation_obj.browse(cr, uid, new_operation, context=context)
1467 pack_operation_ids.append(op.id)
1468 if op.product_id and op.location_id and op.location_dest_id:
1469 stock_move_obj.check_tracking_product(cr, uid, op.product_id, op.lot_id.id, op.location_id, op.location_dest_id, context=context)
1470 package_id = package_obj.create(cr, uid, {}, context=context)
1471 stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context)
1474 def process_product_id_from_ui(self, cr, uid, picking_id, product_id, op_id, increment=True, context=None):
1475 return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id),('id', '=', op_id)], increment=increment, context=context)
1477 def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, visible_op_ids, context=None):
1478 '''This function is called each time there barcode scanner reads an input'''
1479 lot_obj = self.pool.get('stock.production.lot')
1480 package_obj = self.pool.get('stock.quant.package')
1481 product_obj = self.pool.get('product.product')
1482 stock_operation_obj = self.pool.get('stock.pack.operation')
1483 stock_location_obj = self.pool.get('stock.location')
1484 answer = {'filter_loc': False, 'operation_id': False}
1485 #check if the barcode correspond to a location
1486 matching_location_ids = stock_location_obj.search(cr, uid, [('loc_barcode', '=', barcode_str)], context=context)
1487 if matching_location_ids:
1488 #if we have a location, return immediatly with the location name
1489 location = stock_location_obj.browse(cr, uid, matching_location_ids[0], context=None)
1490 answer['filter_loc'] = stock_location_obj._name_get(cr, uid, location, context=None)
1491 answer['filter_loc_id'] = matching_location_ids[0]
1493 #check if the barcode correspond to a product
1494 matching_product_ids = product_obj.search(cr, uid, ['|', ('ean13', '=', barcode_str), ('default_code', '=', barcode_str)], context=context)
1495 if matching_product_ids:
1496 op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', matching_product_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
1497 answer['operation_id'] = op_id
1499 #check if the barcode correspond to a lot
1500 matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1501 if matching_lot_ids:
1502 lot = lot_obj.browse(cr, uid, matching_lot_ids[0], context=context)
1503 op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
1504 answer['operation_id'] = op_id
1506 #check if the barcode correspond to a package
1507 matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1508 if matching_package_ids:
1509 op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
1510 answer['operation_id'] = op_id
1515 class stock_production_lot(osv.osv):
1516 _name = 'stock.production.lot'
1517 _inherit = ['mail.thread']
1518 _description = 'Lot/Serial'
1520 'name': fields.char('Serial Number', required=True, help="Unique Serial Number"),
1521 'ref': fields.char('Internal Reference', help="Internal reference number in case it differs from the manufacturer's serial number"),
1522 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1523 'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants', readonly=True),
1524 'create_date': fields.datetime('Creation Date'),
1527 'name': lambda x, y, z, c: x.pool.get('ir.sequence').next_by_code(y, z, 'stock.lot.serial'),
1528 'product_id': lambda x, y, z, c: c.get('product_id', False),
1530 _sql_constraints = [
1531 ('name_ref_uniq', 'unique (name, ref, product_id)', 'The combination of serial number, internal reference and product must be unique !'),
1534 def action_traceability(self, cr, uid, ids, context=None):
1535 """ It traces the information of lots
1536 @param self: The object pointer.
1537 @param cr: A database cursor
1538 @param uid: ID of the user currently logged in
1539 @param ids: List of IDs selected
1540 @param context: A standard dictionary
1541 @return: A dictionary of values
1543 quant_obj = self.pool.get("stock.quant")
1544 quants = quant_obj.search(cr, uid, [('lot_id', 'in', ids)], context=context)
1546 for quant in quant_obj.browse(cr, uid, quants, context=context):
1547 moves |= {move.id for move in quant.history_ids}
1550 'domain': "[('id','in',[" + ','.join(map(str, list(moves))) + "])]",
1551 'name': _('Traceability'),
1552 'view_mode': 'tree,form',
1553 'view_type': 'form',
1554 'context': {'tree_view_ref': 'stock.view_move_tree'},
1555 'res_model': 'stock.move',
1556 'type': 'ir.actions.act_window',
1561 # ----------------------------------------------------
1563 # ----------------------------------------------------
1565 class stock_move(osv.osv):
1566 _name = "stock.move"
1567 _description = "Stock Move"
1568 _order = 'date_expected desc, id'
1571 def get_price_unit(self, cr, uid, move, context=None):
1572 """ Returns the unit price to store on the quant """
1573 return move.price_unit or move.product_id.standard_price
1575 def name_get(self, cr, uid, ids, context=None):
1577 for line in self.browse(cr, uid, ids, context=context):
1578 name = line.location_id.name + ' > ' + line.location_dest_id.name
1579 if line.product_id.code:
1580 name = line.product_id.code + ': ' + name
1581 if line.picking_id.origin:
1582 name = line.picking_id.origin + '/ ' + name
1583 res.append((line.id, name))
1586 def _quantity_normalize(self, cr, uid, ids, name, args, context=None):
1587 uom_obj = self.pool.get('product.uom')
1589 for m in self.browse(cr, uid, ids, context=context):
1590 res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, context=context)
1593 def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
1594 uom_obj = self.pool.get('product.uom')
1596 for move in self.browse(cr, uid, ids, context=context):
1597 qty = move.product_qty
1598 for record in move.linked_move_operation_ids:
1600 # Keeping in product default UoM
1601 res[move.id] = float_round(qty, precision_rounding=move.product_id.uom_id.rounding)
1604 def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
1605 res = dict.fromkeys(ids, False)
1606 for move in self.browse(cr, uid, ids, context=context):
1607 if move.state == 'done':
1608 res[move.id] = [q.lot_id.id for q in move.quant_ids if q.lot_id]
1610 res[move.id] = [q.lot_id.id for q in move.reserved_quant_ids if q.lot_id]
1613 def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
1614 quant_obj = self.pool.get('stock.quant')
1615 res = dict.fromkeys(ids, False)
1616 for move in self.browse(cr, uid, ids, context=context):
1617 if move.state == 'done':
1618 res[move.id] = move.product_qty
1620 sublocation_ids = self.pool.get('stock.location').search(cr, uid, [('id', 'child_of', [move.location_id.id])], context=context)
1621 quant_ids = quant_obj.search(cr, uid, [('location_id', 'in', sublocation_ids), ('product_id', '=', move.product_id.id), ('reservation_id', '=', False)], context=context)
1623 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1624 availability += quant.qty
1625 res[move.id] = min(move.product_qty, availability)
1628 def _get_string_qty_information(self, cr, uid, ids, field_name, args, context=None):
1629 settings_obj = self.pool.get('stock.config.settings')
1630 uom_obj = self.pool.get('product.uom')
1631 res = dict.fromkeys(ids, '')
1632 for move in self.browse(cr, uid, ids, context=context):
1633 if move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal':
1634 res[move.id] = '' # 'not applicable' or 'n/a' could work too
1636 total_available = min(move.product_qty, move.reserved_availability + move.availability)
1637 total_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, total_available, move.product_uom, context=context)
1638 info = str(total_available)
1639 #look in the settings if we need to display the UoM name or not
1640 config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
1642 stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
1643 if stock_settings.group_uom:
1644 info += ' ' + move.product_uom.name
1645 if move.reserved_availability:
1646 if move.reserved_availability != total_available:
1647 #some of the available quantity is assigned and some are available but not reserved
1648 reserved_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, move.reserved_availability, move.product_uom, context=context)
1649 info += _(' (%s reserved)') % str(reserved_available)
1651 #all available quantity is assigned
1652 info += _(' (reserved)')
1656 def _get_reserved_availability(self, cr, uid, ids, field_name, args, context=None):
1657 res = dict.fromkeys(ids, 0)
1658 for move in self.browse(cr, uid, ids, context=context):
1659 res[move.id] = sum([quant.qty for quant in move.reserved_quant_ids])
1662 def _get_move(self, cr, uid, ids, context=None):
1664 for quant in self.browse(cr, uid, ids, context=context):
1665 if quant.reservation_id:
1666 res.add(quant.reservation_id.id)
1669 def _get_move_ids(self, cr, uid, ids, context=None):
1671 for picking in self.browse(cr, uid, ids, context=context):
1672 res += [x.id for x in picking.move_lines]
1675 def _get_moves_from_prod(self, cr, uid, ids, context=None):
1677 return self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', ids)], context=context)
1680 def _set_product_qty(self, cr, uid, id, field, value, arg, context=None):
1681 """ The meaning of product_qty field changed lately and is now a functional field computing the quantity
1682 in the default product UoM. This code has been added to raise an error if a write is made given a value
1683 for `product_qty`, where the same write should set the `product_uom_qty` field instead, in order to
1686 raise osv.except_osv(_('Programming Error!'), _('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
1689 'name': fields.char('Description', required=True, select=True),
1690 'priority': fields.selection(procurement.PROCUREMENT_PRIORITIES, 'Priority'),
1691 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1692 '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)]}),
1693 'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"),
1694 'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}),
1695 'product_qty': fields.function(_quantity_normalize, fnct_inv=_set_product_qty, type='float', digits=0, store={
1696 'stock.move': (lambda self, cr, uid, ids, ctx: ids, ['product_id', 'product_uom_qty', 'product_uom'], 20),
1697 'product.product': (_get_moves_from_prod, ['uom_id'], 20),
1698 }, string='Quantity',
1699 help='Quantity in the default UoM of the product'),
1700 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
1701 required=True, states={'done': [('readonly', True)]},
1702 help="This is the quantity of products from an inventory "
1703 "point of view. For moves in the state 'done', this is the "
1704 "quantity of products that were actually moved. For other "
1705 "moves, this is the quantity of product that is planned to "
1706 "be moved. Lowering this quantity does not generate a "
1707 "backorder. Changing this quantity on assigned moves affects "
1708 "the product reservation, and should be done with care."
1710 'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]}),
1711 'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}),
1712 'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1714 'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1716 '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."),
1717 '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."),
1719 '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"),
1722 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True, copy=False),
1723 'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
1725 'picking_id': fields.many2one('stock.picking', 'Transfer Reference', select=True, states={'done': [('readonly', True)]}),
1726 'note': fields.text('Notes'),
1727 'state': fields.selection([('draft', 'New'),
1728 ('cancel', 'Cancelled'),
1729 ('waiting', 'Waiting Another Move'),
1730 ('confirmed', 'Waiting Availability'),
1731 ('assigned', 'Available'),
1733 ], 'Status', readonly=True, select=True, copy=False,
1734 help= "* New: When the stock move is created and not yet confirmed.\n"\
1735 "* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"\
1736 "* 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"\
1737 "* Available: When products are reserved, it is set to \'Available\'.\n"\
1738 "* Done: When the shipment is processed, the state is \'Done\'."),
1739 'partially_available': fields.boolean('Partially Available', readonly=True, help="Checks if the move has some stock reserved", copy=False),
1740 'price_unit': fields.float('Unit Price', help="Technical field used to record the product cost set by the user during a picking confirmation (when costing method used is 'average price' or 'real'). Value given in company currency and in product uom."), # as it's a technical field, we intentionally don't provide the digits attribute
1742 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1743 'split_from': fields.many2one('stock.move', string="Move Split From", help="Technical field used to track the origin of a split move, which can be useful in case of debug", copy=False),
1744 'backorder_id': fields.related('picking_id', 'backorder_id', type='many2one', relation="stock.picking", string="Back Order of", select=True),
1745 'origin': fields.char("Source Document"),
1746 'procure_method': fields.selection([('make_to_stock', 'Default: Take From Stock'), ('make_to_order', 'Advanced: Apply Procurement Rules')], 'Supply Method', required=True,
1747 help="""By default, the system will take from the stock in the source location and passively wait for availability. The other possibility allows you to directly create a procurement on the source location (and thus ignore its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, this second option should be chosen."""),
1749 # used for colors in tree views:
1750 'scrapped': fields.related('location_dest_id', 'scrap_location', type='boolean', relation='stock.location', string='Scrapped', readonly=True),
1752 'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Moved Quants'),
1753 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
1754 'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'move_id', string='Linked Operations', readonly=True, help='Operations that impact this move for the computation of the remaining quantities'),
1755 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity', digits=0,
1756 states={'done': [('readonly', True)]}, help="Remaining Quantity in default UoM according to operations matched with this move"),
1757 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1758 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
1759 'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'),
1760 'push_rule_id': fields.many2one('stock.location.path', 'Push Rule', help='The push rule that created this stock move'),
1761 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
1762 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
1763 'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
1764 'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.production.lot', string='Lots'),
1765 'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move', copy=False),
1766 'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
1767 'reserved_availability': fields.function(_get_reserved_availability, type='float', string='Quantity Reserved', readonly=True, help='Quantity that has already been reserved for this move'),
1768 'availability': fields.function(_get_product_availability, type='float', string='Quantity Available', readonly=True, help='Quantity in stock that can still be reserved for this move'),
1769 'string_availability_info': fields.function(_get_string_qty_information, type='text', string='Availability', readonly=True, help='Show various information on stock availability for this move'),
1770 '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'"),
1771 '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'"),
1772 '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"),
1773 '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)."),
1776 def _default_location_destination(self, cr, uid, context=None):
1777 context = context or {}
1778 if context.get('default_picking_type_id', False):
1779 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1780 return pick_type.default_location_dest_id and pick_type.default_location_dest_id.id or False
1783 def _default_location_source(self, cr, uid, context=None):
1784 context = context or {}
1785 if context.get('default_picking_type_id', False):
1786 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1787 return pick_type.default_location_src_id and pick_type.default_location_src_id.id or False
1790 def _default_destination_address(self, cr, uid, context=None):
1791 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1792 return user.company_id.partner_id.id
1795 'location_id': _default_location_source,
1796 'location_dest_id': _default_location_destination,
1797 'partner_id': _default_destination_address,
1800 'product_uom_qty': 1.0,
1802 'date': fields.datetime.now,
1803 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1804 'date_expected': fields.datetime.now,
1805 'procure_method': 'make_to_stock',
1807 'partially_available': False,
1810 def _check_uom(self, cr, uid, ids, context=None):
1811 for move in self.browse(cr, uid, ids, context=context):
1812 if move.product_id.uom_id.category_id.id != move.product_uom.category_id.id:
1818 '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.',
1822 @api.cr_uid_ids_context
1823 def do_unreserve(self, cr, uid, move_ids, context=None):
1824 quant_obj = self.pool.get("stock.quant")
1825 for move in self.browse(cr, uid, move_ids, context=context):
1826 if move.state in ('done', 'cancel'):
1827 raise osv.except_osv(_('Operation Forbidden!'), _('Cannot unreserve a done move'))
1828 quant_obj.quants_unreserve(cr, uid, move, context=context)
1829 if self.find_move_ancestors(cr, uid, move, context=context):
1830 self.write(cr, uid, [move.id], {'state': 'waiting'}, context=context)
1832 self.write(cr, uid, [move.id], {'state': 'confirmed'}, context=context)
1834 def _prepare_procurement_from_move(self, cr, uid, move, context=None):
1835 origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or move.origin or "/")
1836 group_id = move.group_id and move.group_id.id or False
1838 if move.rule_id.group_propagation_option == 'fixed' and move.rule_id.group_id:
1839 group_id = move.rule_id.group_id.id
1840 elif move.rule_id.group_propagation_option == 'none':
1843 'name': move.rule_id and move.rule_id.name or "/",
1845 'company_id': move.company_id and move.company_id.id or False,
1846 'date_planned': move.date,
1847 'product_id': move.product_id.id,
1848 'product_qty': move.product_uom_qty,
1849 'product_uom': move.product_uom.id,
1850 'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_uom_qty,
1851 'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
1852 'location_id': move.location_id.id,
1853 'move_dest_id': move.id,
1854 'group_id': group_id,
1855 'route_ids': [(4, x.id) for x in move.route_ids],
1856 'warehouse_id': move.warehouse_id.id or (move.picking_type_id and move.picking_type_id.warehouse_id.id or False),
1857 'priority': move.priority,
1860 def _push_apply(self, cr, uid, moves, context=None):
1861 push_obj = self.pool.get("stock.location.path")
1863 #1) if the move is already chained, there is no need to check push rules
1864 #2) if the move is a returned move, we don't want to check push rules, as returning a returned move is the only decent way
1865 # to receive goods without triggering the push rules again (which would duplicate chained operations)
1866 if not move.move_dest_id and not move.origin_returned_move_id:
1867 domain = [('location_from_id', '=', move.location_dest_id.id)]
1868 #priority goes to the route defined on the product and product category
1869 route_ids = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids]
1870 rules = push_obj.search(cr, uid, domain + [('route_id', 'in', route_ids)], order='route_sequence, sequence', context=context)
1872 #then we search on the warehouse if a rule can apply
1874 if move.warehouse_id:
1875 wh_route_ids = [x.id for x in move.warehouse_id.route_ids]
1876 elif move.picking_type_id and move.picking_type_id.warehouse_id:
1877 wh_route_ids = [x.id for x in move.picking_type_id.warehouse_id.route_ids]
1879 rules = push_obj.search(cr, uid, domain + [('route_id', 'in', wh_route_ids)], order='route_sequence, sequence', context=context)
1881 #if no specialized push rule has been found yet, we try to find a general one (without route)
1882 rules = push_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context)
1884 rule = push_obj.browse(cr, uid, rules[0], context=context)
1885 push_obj._apply(cr, uid, rule, move, context=context)
1888 def _create_procurement(self, cr, uid, move, context=None):
1889 """ This will create a procurement order """
1890 return self.pool.get("procurement.order").create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
1892 def write(self, cr, uid, ids, vals, context=None):
1895 if isinstance(ids, (int, long)):
1897 # Check that we do not modify a stock.move which is done
1898 frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1899 for move in self.browse(cr, uid, ids, context=context):
1900 if move.state == 'done':
1901 if frozen_fields.intersection(vals):
1902 raise osv.except_osv(_('Operation Forbidden!'),
1903 _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
1904 propagated_changes_dict = {}
1905 #propagation of quantity change
1906 if vals.get('product_uom_qty'):
1907 propagated_changes_dict['product_uom_qty'] = vals['product_uom_qty']
1908 if vals.get('product_uom_id'):
1909 propagated_changes_dict['product_uom_id'] = vals['product_uom_id']
1910 #propagation of expected date:
1911 propagated_date_field = False
1912 if vals.get('date_expected'):
1913 #propagate any manual change of the expected date
1914 propagated_date_field = 'date_expected'
1915 elif (vals.get('state', '') == 'done' and vals.get('date')):
1916 #propagate also any delta observed when setting the move as done
1917 propagated_date_field = 'date'
1919 if not context.get('do_not_propagate', False) and (propagated_date_field or propagated_changes_dict):
1920 #any propagation is (maybe) needed
1921 for move in self.browse(cr, uid, ids, context=context):
1922 if move.move_dest_id and move.propagate:
1923 if 'date_expected' in propagated_changes_dict:
1924 propagated_changes_dict.pop('date_expected')
1925 if propagated_date_field:
1926 current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
1927 new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT)
1928 delta = new_date - current_date
1929 if abs(delta.days) >= move.company_id.propagation_minimum_delta:
1930 old_move_date = datetime.strptime(move.move_dest_id.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
1931 new_move_date = (old_move_date + relativedelta.relativedelta(days=delta.days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1932 propagated_changes_dict['date_expected'] = new_move_date
1933 #For pushed moves as well as for pulled moves, propagate by recursive call of write().
1934 #Note that, for pulled moves we intentionally don't propagate on the procurement.
1935 if propagated_changes_dict:
1936 self.write(cr, uid, [move.move_dest_id.id], propagated_changes_dict, context=context)
1937 return super(stock_move, self).write(cr, uid, ids, vals, context=context)
1939 def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1940 """ On change of product quantity finds UoM and UoS quantities
1941 @param product_id: Product id
1942 @param product_qty: Changed Quantity of product
1943 @param product_uom: Unit of measure of product
1944 @param product_uos: Unit of sale of product
1945 @return: Dictionary of values
1948 'product_uos_qty': 0.00
1952 if (not product_id) or (product_qty <= 0.0):
1953 result['product_qty'] = 0.0
1954 return {'value': result}
1956 product_obj = self.pool.get('product.product')
1957 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1959 # Warn if the quantity was decreased
1961 for move in self.read(cr, uid, ids, ['product_qty']):
1962 if product_qty < move['product_qty']:
1964 'title': _('Information'),
1965 'message': _("By changing this quantity here, you accept the "
1966 "new quantity as complete: Odoo will not "
1967 "automatically generate a back order.")})
1970 if product_uos and product_uom and (product_uom != product_uos):
1971 result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1973 result['product_uos_qty'] = product_qty
1975 return {'value': result, 'warning': warning}
1977 def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1978 product_uos, product_uom):
1979 """ On change of product quantity finds UoM and UoS quantities
1980 @param product_id: Product id
1981 @param product_uos_qty: Changed UoS Quantity of product
1982 @param product_uom: Unit of measure of product
1983 @param product_uos: Unit of sale of product
1984 @return: Dictionary of values
1987 'product_uom_qty': 0.00
1990 if (not product_id) or (product_uos_qty <= 0.0):
1991 result['product_uos_qty'] = 0.0
1992 return {'value': result}
1994 product_obj = self.pool.get('product.product')
1995 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1997 # No warning if the quantity was decreased to avoid double warnings:
1998 # The clients should call onchange_quantity too anyway
2000 if product_uos and product_uom and (product_uom != product_uos):
2001 result['product_uom_qty'] = product_uos_qty / uos_coeff['uos_coeff']
2003 result['product_uom_qty'] = product_uos_qty
2004 return {'value': result}
2006 def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, partner_id=False):
2007 """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
2008 @param prod_id: Changed Product id
2009 @param loc_id: Source location id
2010 @param loc_dest_id: Destination location id
2011 @param partner_id: Address id of partner
2012 @return: Dictionary of values
2016 user = self.pool.get('res.users').browse(cr, uid, uid)
2017 lang = user and user.lang or False
2019 addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id)
2021 lang = addr_rec and addr_rec.lang or False
2022 ctx = {'lang': lang}
2024 product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
2025 uos_id = product.uos_id and product.uos_id.id or False
2027 'name': product.partner_ref,
2028 'product_uom': product.uom_id.id,
2029 'product_uos': uos_id,
2030 'product_uom_qty': 1.00,
2031 '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'],
2034 result['location_id'] = loc_id
2036 result['location_dest_id'] = loc_dest_id
2037 return {'value': result}
2039 @api.cr_uid_ids_context
2040 def _picking_assign(self, cr, uid, move_ids, procurement_group, location_from, location_to, context=None):
2041 """Assign a picking on the given move_ids, which is a list of move supposed to share the same procurement_group, location_from and location_to
2042 (and company). Those attributes are also given as parameters.
2044 pick_obj = self.pool.get("stock.picking")
2045 picks = pick_obj.search(cr, uid, [
2046 ('group_id', '=', procurement_group),
2047 ('location_id', '=', location_from),
2048 ('location_dest_id', '=', location_to),
2049 ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context)
2053 move = self.browse(cr, uid, move_ids, context=context)[0]
2055 'origin': move.origin,
2056 'company_id': move.company_id and move.company_id.id or False,
2057 'move_type': move.group_id and move.group_id.move_type or 'direct',
2058 'partner_id': move.partner_id.id or False,
2059 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
2061 pick = pick_obj.create(cr, uid, values, context=context)
2062 return self.write(cr, uid, move_ids, {'picking_id': pick}, context=context)
2064 def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
2065 """ On change of Scheduled Date gives a Move date.
2066 @param date_expected: Scheduled Date
2067 @param date: Move Date
2070 if not date_expected:
2071 date_expected = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
2072 return {'value': {'date': date_expected}}
2074 def attribute_price(self, cr, uid, move, context=None):
2076 Attribute price to move, important in inter-company moves or receipts with only one partner
2078 if not move.price_unit:
2079 price = move.product_id.standard_price
2080 self.write(cr, uid, [move.id], {'price_unit': price})
2082 def action_confirm(self, cr, uid, ids, context=None):
2083 """ Confirms stock move or put it in waiting if it's linked to another move.
2084 @return: List of ids.
2086 if isinstance(ids, (int, long)):
2093 for move in self.browse(cr, uid, ids, context=context):
2094 self.attribute_price(cr, uid, move, context=context)
2096 #if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available)
2097 if move.move_orig_ids:
2099 #if the move is split and some of the ancestor was preceeded, then it's waiting as well
2100 elif move.split_from:
2101 move2 = move.split_from
2102 while move2 and state != 'waiting':
2103 if move2.move_orig_ids:
2105 move2 = move2.split_from
2106 states[state].append(move.id)
2108 if not move.picking_id and move.picking_type_id:
2109 key = (move.group_id.id, move.location_id.id, move.location_dest_id.id)
2110 if key not in to_assign:
2112 to_assign[key].append(move.id)
2114 for move in self.browse(cr, uid, states['confirmed'], context=context):
2115 if move.procure_method == 'make_to_order':
2116 self._create_procurement(cr, uid, move, context=context)
2117 states['waiting'].append(move.id)
2118 states['confirmed'].remove(move.id)
2120 for state, write_ids in states.items():
2122 self.write(cr, uid, write_ids, {'state': state})
2123 #assign picking in batch for all confirmed move that share the same details
2124 for key, move_ids in to_assign.items():
2125 procurement_group, location_from, location_to = key
2126 self._picking_assign(cr, uid, move_ids, procurement_group, location_from, location_to, context=context)
2127 moves = self.browse(cr, uid, ids, context=context)
2128 self._push_apply(cr, uid, moves, context=context)
2131 def force_assign(self, cr, uid, ids, context=None):
2132 """ Changes the state to assigned.
2135 return self.write(cr, uid, ids, {'state': 'assigned'}, context=context)
2137 def check_tracking_product(self, cr, uid, product, lot_id, location, location_dest, context=None):
2139 if product.track_all and not location_dest.usage == 'inventory':
2141 elif product.track_incoming and location.usage in ('supplier', 'transit', 'inventory') and location_dest.usage == 'internal':
2143 elif product.track_outgoing and location_dest.usage in ('customer', 'transit') and location.usage == 'internal':
2145 if check and not lot_id:
2146 raise osv.except_osv(_('Warning!'), _('You must assign a serial number for the product %s') % (product.name))
2149 def check_tracking(self, cr, uid, move, lot_id, context=None):
2150 """ Checks if serial number is assigned to stock move or not and raise an error if it had to.
2152 self.check_tracking_product(cr, uid, move.product_id, lot_id, move.location_id, move.location_dest_id, context=context)
2155 def action_assign(self, cr, uid, ids, context=None):
2156 """ Checks the product type and accordingly writes the state.
2158 context = context or {}
2159 quant_obj = self.pool.get("stock.quant")
2160 to_assign_moves = []
2164 for move in self.browse(cr, uid, ids, context=context):
2165 if move.state not in ('confirmed', 'waiting', 'assigned'):
2167 if move.location_id.usage in ('supplier', 'inventory', 'production'):
2168 to_assign_moves.append(move.id)
2169 #in case the move is returned, we want to try to find quants before forcing the assignment
2170 if not move.origin_returned_move_id:
2172 if move.product_id.type == 'consu':
2173 to_assign_moves.append(move.id)
2176 todo_moves.append(move)
2178 #we always keep the quants already assigned and try to find the remaining quantity on quants not assigned only
2179 main_domain[move.id] = [('reservation_id', '=', False), ('qty', '>', 0)]
2181 #if the move is preceeded, restrict the choice of quants in the ones moved previously in original move
2182 ancestors = self.find_move_ancestors(cr, uid, move, context=context)
2183 if move.state == 'waiting' and not ancestors:
2184 #if the waiting move hasn't yet any ancestor (PO/MO not confirmed yet), don't find any quant available in stock
2185 main_domain[move.id] += [('id', '=', False)]
2187 main_domain[move.id] += [('history_ids', 'in', ancestors)]
2189 #if the move is returned from another, restrict the choice of quants to the ones that follow the returned move
2190 if move.origin_returned_move_id:
2191 main_domain[move.id] += [('history_ids', 'in', move.origin_returned_move_id.id)]
2192 for link in move.linked_move_operation_ids:
2193 operations.add(link.operation_id)
2194 # Check all ops and sort them: we want to process first the packages, then operations with lot then the rest
2195 operations = list(operations)
2196 operations.sort(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
2197 for ops in operations:
2198 #first try to find quants based on specific domains given by linked operations
2199 for record in ops.linked_move_operation_ids:
2200 move = record.move_id
2201 if move.id in main_domain:
2202 domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
2205 quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, qty, domain=domain, prefered_domain_list=[], restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2206 quant_obj.quants_reserve(cr, uid, quants, move, record, context=context)
2207 for move in todo_moves:
2208 if move.linked_move_operation_ids:
2210 #then if the move isn't totally assigned, try to find quants without any specific domain
2211 if move.state != 'assigned':
2212 qty_already_assigned = move.reserved_availability
2213 qty = move.product_qty - qty_already_assigned
2214 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain[move.id], prefered_domain_list=[], restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2215 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
2217 #force assignation of consumable products and incoming from supplier/inventory/production
2219 self.force_assign(cr, uid, to_assign_moves, context=context)
2221 def action_cancel(self, cr, uid, ids, context=None):
2222 """ Cancels the moves and if all moves are cancelled it cancels the picking.
2225 procurement_obj = self.pool.get('procurement.order')
2226 context = context or {}
2228 for move in self.browse(cr, uid, ids, context=context):
2229 if move.state == 'done':
2230 raise osv.except_osv(_('Operation Forbidden!'),
2231 _('You cannot cancel a stock move that has been set to \'Done\'.'))
2232 if move.reserved_quant_ids:
2233 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
2234 if context.get('cancel_procurement'):
2236 procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
2237 procurement_obj.cancel(cr, uid, procurement_ids, context=context)
2239 if move.move_dest_id:
2241 self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
2242 elif move.move_dest_id.state == 'waiting':
2243 #If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
2244 self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context)
2245 if move.procurement_id:
2246 # Does the same as procurement check, only eliminating a refresh
2247 procs_to_check.append(move.procurement_id.id)
2249 res = self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
2251 procurement_obj.check(cr, uid, procs_to_check, context=context)
2254 def _check_package_from_moves(self, cr, uid, ids, context=None):
2255 pack_obj = self.pool.get("stock.quant.package")
2257 for move in self.browse(cr, uid, ids, context=context):
2258 packs |= set([q.package_id for q in move.quant_ids if q.package_id and q.qty > 0])
2259 return pack_obj._check_location_constraint(cr, uid, list(packs), context=context)
2261 def find_move_ancestors(self, cr, uid, move, context=None):
2262 '''Find the first level ancestors of given move '''
2266 ancestors += [x.id for x in move2.move_orig_ids]
2267 #loop on the split_from to find the ancestor of split moves only if the move has not direct ancestor (priority goes to them)
2268 move2 = not move2.move_orig_ids and move2.split_from or False
2271 @api.cr_uid_ids_context
2272 def recalculate_move_state(self, cr, uid, move_ids, context=None):
2273 '''Recompute the state of moves given because their reserved quants were used to fulfill another operation'''
2274 for move in self.browse(cr, uid, move_ids, context=context):
2276 reserved_quant_ids = move.reserved_quant_ids
2277 if len(reserved_quant_ids) > 0 and not move.partially_available:
2278 vals['partially_available'] = True
2279 if len(reserved_quant_ids) == 0 and move.partially_available:
2280 vals['partially_available'] = False
2281 if move.state == 'assigned':
2282 if self.find_move_ancestors(cr, uid, move, context=context):
2283 vals['state'] = 'waiting'
2285 vals['state'] = 'confirmed'
2287 self.write(cr, uid, [move.id], vals, context=context)
2289 def action_done(self, cr, uid, ids, context=None):
2290 """ Process completely the moves given as ids and if all moves are done, it will finish the picking.
2292 context = context or {}
2293 picking_obj = self.pool.get("stock.picking")
2294 quant_obj = self.pool.get("stock.quant")
2295 todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
2297 ids = self.action_confirm(cr, uid, todo, context=context)
2299 procurement_ids = []
2300 #Search operations that are linked to the moves
2303 for move in self.browse(cr, uid, ids, context=context):
2304 move_qty[move.id] = move.product_qty
2305 for link in move.linked_move_operation_ids:
2306 operations.add(link.operation_id)
2308 #Sort operations according to entire packages first, then package + lot, package only, lot only
2309 operations = list(operations)
2310 operations.sort(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
2312 for ops in operations:
2314 pickings.add(ops.picking_id.id)
2315 main_domain = [('qty', '>', 0)]
2316 for record in ops.linked_move_operation_ids:
2317 move = record.move_id
2318 self.check_tracking(cr, uid, move, not ops.product_id and ops.package_id.id or ops.lot_id.id, context=context)
2319 prefered_domain = [('reservation_id', '=', move.id)]
2320 fallback_domain = [('reservation_id', '=', False)]
2321 fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
2322 prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
2323 dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
2324 quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain_list=prefered_domain_list,
2325 restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2326 if ops.result_package_id.id:
2327 #if a result package is given, all quants go there
2328 quant_dest_package_id = ops.result_package_id.id
2329 elif ops.product_id and ops.package_id:
2330 #if a package and a product is given, we will remove quants from the pack.
2331 quant_dest_package_id = False
2333 #otherwise we keep the current pack of the quant, which may mean None
2334 quant_dest_package_id = ops.package_id.id
2335 quant_obj.quants_move(cr, uid, quants, move, ops.location_dest_id, location_from=ops.location_id, lot_id=ops.lot_id.id, owner_id=ops.owner_id.id, src_package_id=ops.package_id.id, dest_package_id=quant_dest_package_id, context=context)
2336 # Handle pack in pack
2337 if not ops.product_id and ops.package_id and ops.result_package_id.id != ops.package_id.parent_id.id:
2338 self.pool.get('stock.quant.package').write(cr, SUPERUSER_ID, [ops.package_id.id], {'parent_id': ops.result_package_id.id}, context=context)
2339 if not move_qty.get(move.id):
2340 raise osv.except_osv(_("Error"), _("The roundings of your Unit of Measures %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. ") % (move.product_uom.name, move.product_id.uom_id.name))
2341 move_qty[move.id] -= record.qty
2342 #Check for remaining qtys and unreserve/check move_dest_id in
2343 move_dest_ids = set()
2344 for move in self.browse(cr, uid, ids, context=context):
2345 move_qty_cmp = float_compare(move_qty[move.id], 0, precision_rounding=move.product_id.uom_id.rounding)
2346 if move_qty_cmp > 0: # (=In case no pack operations in picking)
2347 main_domain = [('qty', '>', 0)]
2348 prefered_domain = [('reservation_id', '=', move.id)]
2349 fallback_domain = [('reservation_id', '=', False)]
2350 fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
2351 prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
2352 self.check_tracking(cr, uid, move, move.restrict_lot_id.id, context=context)
2353 qty = move_qty[move.id]
2354 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain, prefered_domain_list=prefered_domain_list, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2355 quant_obj.quants_move(cr, uid, quants, move, move.location_dest_id, lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id, context=context)
2357 # If the move has a destination, add it to the list to reserve
2358 if move.move_dest_id and move.move_dest_id.state in ('waiting', 'confirmed'):
2359 move_dest_ids.add(move.move_dest_id.id)
2361 if move.procurement_id:
2362 procurement_ids.append(move.procurement_id.id)
2364 #unreserve the quants and make them available for other operations/moves
2365 quant_obj.quants_unreserve(cr, uid, move, context=context)
2366 # Check the packages have been placed in the correct locations
2367 self._check_package_from_moves(cr, uid, ids, context=context)
2368 #set the move as done
2369 self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2370 self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
2371 #assign destination moves
2373 self.action_assign(cr, uid, list(move_dest_ids), context=context)
2374 #check picking state to set the date_done is needed
2376 for picking in picking_obj.browse(cr, uid, list(pickings), context=context):
2377 if picking.state == 'done' and not picking.date_done:
2378 done_picking.append(picking.id)
2380 picking_obj.write(cr, uid, done_picking, {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2383 def unlink(self, cr, uid, ids, context=None):
2384 context = context or {}
2385 for move in self.browse(cr, uid, ids, context=context):
2386 if move.state not in ('draft', 'cancel'):
2387 raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
2388 return super(stock_move, self).unlink(cr, uid, ids, context=context)
2390 def action_scrap(self, cr, uid, ids, quantity, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
2391 """ Move the scrap/damaged product into scrap location
2392 @param cr: the database cursor
2393 @param uid: the user id
2394 @param ids: ids of stock move object to be scrapped
2395 @param quantity : specify scrap qty
2396 @param location_id : specify scrap location
2397 @param context: context arguments
2398 @return: Scraped lines
2400 #quantity should be given in MOVE UOM
2402 raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
2404 for move in self.browse(cr, uid, ids, context=context):
2405 source_location = move.location_id
2406 if move.state == 'done':
2407 source_location = move.location_dest_id
2408 #Previously used to prevent scraping from virtual location but not necessary anymore
2409 #if source_location.usage != 'internal':
2410 #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
2411 #raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
2412 move_qty = move.product_qty
2413 uos_qty = quantity / move_qty * move.product_uos_qty
2415 'location_id': source_location.id,
2416 'product_uom_qty': quantity,
2417 'product_uos_qty': uos_qty,
2418 'state': move.state,
2420 'location_dest_id': location_id,
2421 'restrict_lot_id': restrict_lot_id,
2422 'restrict_partner_id': restrict_partner_id,
2424 new_move = self.copy(cr, uid, move.id, default_val)
2427 product_obj = self.pool.get('product.product')
2428 for product in product_obj.browse(cr, uid, [move.product_id.id], context=context):
2430 uom = product.uom_id.name if product.uom_id else ''
2431 message = _("%s %s %s has been <b>moved to</b> scrap.") % (quantity, uom, product.name)
2432 move.picking_id.message_post(body=message)
2434 self.action_done(cr, uid, res, context=context)
2437 def split(self, cr, uid, move, qty, restrict_lot_id=False, restrict_partner_id=False, context=None):
2438 """ Splits qty from move move into a new move
2439 :param move: browse record
2440 :param qty: float. quantity to split (given in product UoM)
2441 :param restrict_lot_id: optional production lot that can be given in order to force the new move to restrict its choice of quants to this lot.
2442 :param restrict_partner_id: optional partner that can be given in order to force the new move to restrict its choice of quants to the ones belonging to this partner.
2443 :param context: dictionay. can contains the special key 'source_location_id' in order to force the source location when copying the move
2445 returns the ID of the backorder move created
2447 if move.state in ('done', 'cancel'):
2448 raise osv.except_osv(_('Error'), _('You cannot split a move done'))
2449 if move.state == 'draft':
2450 #we restrict the split of a draft move because if not confirmed yet, it may be replaced by several other moves in
2451 #case of phantom bom (with mrp module). And we don't want to deal with this complexity by copying the product that will explode.
2452 raise osv.except_osv(_('Error'), _('You cannot split a draft move. It needs to be confirmed first.'))
2454 if move.product_qty <= qty or qty == 0:
2457 uom_obj = self.pool.get('product.uom')
2458 context = context or {}
2460 #HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
2461 uom_qty = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom, rounding_method='HALF-UP', context=context)
2462 uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
2465 'product_uom_qty': uom_qty,
2466 'product_uos_qty': uos_qty,
2467 'procure_method': 'make_to_stock',
2468 'restrict_lot_id': restrict_lot_id,
2469 'restrict_partner_id': restrict_partner_id,
2470 'split_from': move.id,
2471 'procurement_id': move.procurement_id.id,
2472 'move_dest_id': move.move_dest_id.id,
2474 if context.get('source_location_id'):
2475 defaults['location_id'] = context['source_location_id']
2476 new_move = self.copy(cr, uid, move.id, defaults)
2478 ctx = context.copy()
2479 ctx['do_not_propagate'] = True
2480 self.write(cr, uid, [move.id], {
2481 'product_uom_qty': move.product_uom_qty - uom_qty,
2482 'product_uos_qty': move.product_uos_qty - uos_qty,
2485 if move.move_dest_id and move.propagate and move.move_dest_id.state not in ('done', 'cancel'):
2486 new_move_prop = self.split(cr, uid, move.move_dest_id, qty, context=context)
2487 self.write(cr, uid, [new_move], {'move_dest_id': new_move_prop}, context=context)
2488 #returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
2489 #thus the result of action_confirm should always be a list of 1 element length)
2490 return self.action_confirm(cr, uid, [new_move], context=context)[0]
2493 def get_code_from_locs(self, cr, uid, move, location_id=False, location_dest_id=False, context=None):
2495 Returns the code the picking type should have. This can easily be used
2496 to check if a move is internal or not
2497 move, location_id and location_dest_id are browse records
2500 src_loc = location_id or move.location_id
2501 dest_loc = location_dest_id or move.location_dest_id
2502 if src_loc.usage == 'internal' and dest_loc.usage != 'internal':
2504 if src_loc.usage != 'internal' and dest_loc.usage == 'internal':
2509 class stock_inventory(osv.osv):
2510 _name = "stock.inventory"
2511 _description = "Inventory"
2513 def _get_move_ids_exist(self, cr, uid, ids, field_name, arg, context=None):
2515 for inv in self.browse(cr, uid, ids, context=context):
2521 def _get_available_filters(self, cr, uid, context=None):
2523 This function will return the list of filter allowed according to the options checked
2524 in 'Settings\Warehouse'.
2526 :rtype: list of tuple
2528 #default available choices
2529 res_filter = [('none', _('All products')), ('product', _('One product only'))]
2530 settings_obj = self.pool.get('stock.config.settings')
2531 config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
2532 #If we don't have updated config until now, all fields are by default false and so should be not dipslayed
2536 stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
2537 if stock_settings.group_stock_tracking_owner:
2538 res_filter.append(('owner', _('One owner only')))
2539 res_filter.append(('product_owner', _('One product for a specific owner')))
2540 if stock_settings.group_stock_tracking_lot:
2541 res_filter.append(('lot', _('One Lot/Serial Number')))
2542 if stock_settings.group_stock_packaging:
2543 res_filter.append(('pack', _('A Pack')))
2546 def _get_total_qty(self, cr, uid, ids, field_name, args, context=None):
2548 for inv in self.browse(cr, uid, ids, context=context):
2549 res[inv.id] = sum([x.product_qty for x in inv.line_ids])
2552 INVENTORY_STATE_SELECTION = [
2554 ('cancel', 'Cancelled'),
2555 ('confirm', 'In Progress'),
2556 ('done', 'Validated'),
2560 'name': fields.char('Inventory Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
2561 'date': fields.datetime('Inventory Date', required=True, readonly=True, help="The date that will be used for the stock level check of the products and the validation of the stock move related to this inventory."),
2562 'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines.", copy=True),
2563 'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves.", states={'done': [('readonly', True)]}),
2564 'state': fields.selection(INVENTORY_STATE_SELECTION, 'Status', readonly=True, select=True, copy=False),
2565 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
2566 'location_id': fields.many2one('stock.location', 'Inventoried Location', required=True, readonly=True, states={'draft': [('readonly', False)]}),
2567 'product_id': fields.many2one('product.product', 'Inventoried Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
2568 'package_id': fields.many2one('stock.quant.package', 'Inventoried Pack', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Pack to focus your inventory on a particular Pack."),
2569 'partner_id': fields.many2one('res.partner', 'Inventoried Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
2570 'lot_id': fields.many2one('stock.production.lot', 'Inventoried Lot/Serial Number', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number.", copy=False),
2571 'move_ids_exist': fields.function(_get_move_ids_exist, type='boolean', string=' Stock Move Exists?', help='technical field for attrs in view'),
2572 'filter': fields.selection(_get_available_filters, 'Selection Filter', required=True),
2573 'total_qty': fields.function(_get_total_qty, type="float"),
2576 def _default_stock_location(self, cr, uid, context=None):
2578 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2579 return warehouse.lot_stock_id.id
2584 'date': fields.datetime.now,
2586 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2587 'location_id': _default_stock_location,
2591 def reset_real_qty(self, cr, uid, ids, context=None):
2592 inventory = self.browse(cr, uid, ids[0], context=context)
2593 line_ids = [line.id for line in inventory.line_ids]
2594 self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
2597 def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2598 """ Creates a stock move from an inventory line
2599 @param inventory_line:
2603 return self.pool.get('stock.move').create(cr, uid, move_vals)
2605 def action_done(self, cr, uid, ids, context=None):
2606 """ Finish the inventory
2609 for inv in self.browse(cr, uid, ids, context=context):
2610 for inventory_line in inv.line_ids:
2611 if inventory_line.product_qty < 0 and inventory_line.product_qty != inventory_line.theoretical_qty:
2612 raise osv.except_osv(_('Warning'), _('You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s' % (inventory_line.product_id.name, inventory_line.product_qty)))
2613 self.action_check(cr, uid, [inv.id], context=context)
2614 self.write(cr, uid, [inv.id], {'state': 'done'}, context=context)
2615 self.post_inventory(cr, uid, inv, context=context)
2618 def post_inventory(self, cr, uid, inv, context=None):
2619 #The inventory is posted as a single step which means quants cannot be moved from an internal location to another using an inventory
2620 #as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior
2621 #as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want).
2622 move_obj = self.pool.get('stock.move')
2623 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids], context=context)
2625 def _create_stock_move(self, cr, uid, inventory, todo_line, context=None):
2626 stock_move_obj = self.pool.get('stock.move')
2627 product_obj = self.pool.get('product.product')
2628 inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id
2630 'name': _('INV:') + (inventory.name or ''),
2631 'product_id': todo_line['product_id'],
2632 'product_uom': todo_line['product_uom_id'],
2633 'date': inventory.date,
2634 'company_id': inventory.company_id.id,
2635 'inventory_id': inventory.id,
2636 'state': 'assigned',
2637 'restrict_lot_id': todo_line.get('prod_lot_id'),
2638 'restrict_partner_id': todo_line.get('partner_id'),
2641 if todo_line['product_qty'] < 0:
2642 #found more than expected
2643 vals['location_id'] = inventory_location_id
2644 vals['location_dest_id'] = todo_line['location_id']
2645 vals['product_uom_qty'] = -todo_line['product_qty']
2647 #found less than expected
2648 vals['location_id'] = todo_line['location_id']
2649 vals['location_dest_id'] = inventory_location_id
2650 vals['product_uom_qty'] = todo_line['product_qty']
2651 return stock_move_obj.create(cr, uid, vals, context=context)
2653 def action_check(self, cr, uid, ids, context=None):
2654 """ Checks the inventory and computes the stock move to do
2657 inventory_line_obj = self.pool.get('stock.inventory.line')
2658 stock_move_obj = self.pool.get('stock.move')
2659 for inventory in self.browse(cr, uid, ids, context=context):
2660 #first remove the existing stock moves linked to this inventory
2661 move_ids = [move.id for move in inventory.move_ids]
2662 stock_move_obj.unlink(cr, uid, move_ids, context=context)
2663 for line in inventory.line_ids:
2664 #compare the checked quantities on inventory lines to the theorical one
2665 inventory_line_obj._resolve_inventory_line(cr, uid, line, context=context)
2667 def action_cancel_draft(self, cr, uid, ids, context=None):
2668 """ Cancels the stock move and change inventory state to draft.
2671 for inv in self.browse(cr, uid, ids, context=context):
2672 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2673 self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
2676 def action_cancel_inventory(self, cr, uid, ids, context=None):
2677 self.action_cancel_draft(cr, uid, ids, context=context)
2679 def prepare_inventory(self, cr, uid, ids, context=None):
2680 inventory_line_obj = self.pool.get('stock.inventory.line')
2681 for inventory in self.browse(cr, uid, ids, context=context):
2682 # If there are inventory lines already (e.g. from import), respect those and set their theoretical qty
2683 line_ids = [line.id for line in inventory.line_ids]
2685 #compute the inventory lines and create them
2686 vals = self._get_inventory_lines(cr, uid, inventory, context=context)
2687 for product_line in vals:
2688 inventory_line_obj.create(cr, uid, product_line, context=context)
2690 # On import calculate theoretical quantity
2691 quant_obj = self.pool.get("stock.quant")
2692 for line in inventory.line_ids:
2693 dom = [('company_id', '=', line.company_id.id), ('location_id', 'child_of', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id),
2694 ('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id)]
2696 dom += [('package_id', '=', line.package_id.id)]
2697 quants = quant_obj.search(cr, uid, dom, context=context)
2699 for quant in quant_obj.browse(cr, uid, quants, context=context):
2700 tot_qty += quant.qty
2701 inventory_line_obj.write(cr, uid, [line.id], {'theoretical_qty': tot_qty}, context=context)
2703 return self.write(cr, uid, ids, {'state': 'confirm', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
2705 def _get_inventory_lines(self, cr, uid, inventory, context=None):
2706 location_obj = self.pool.get('stock.location')
2707 product_obj = self.pool.get('product.product')
2708 location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
2709 domain = ' location_id in %s'
2710 args = (tuple(location_ids),)
2711 if inventory.partner_id:
2712 domain += ' and owner_id = %s'
2713 args += (inventory.partner_id.id,)
2714 if inventory.lot_id:
2715 domain += ' and lot_id = %s'
2716 args += (inventory.lot_id.id,)
2717 if inventory.product_id:
2718 domain += ' and product_id = %s'
2719 args += (inventory.product_id.id,)
2720 if inventory.package_id:
2721 domain += ' and package_id = %s'
2722 args += (inventory.package_id.id,)
2725 SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
2726 FROM stock_quant WHERE''' + domain + '''
2727 GROUP BY product_id, location_id, lot_id, package_id, partner_id
2730 for product_line in cr.dictfetchall():
2731 #replace the None the dictionary by False, because falsy values are tested later on
2732 for key, value in product_line.items():
2734 product_line[key] = False
2735 product_line['inventory_id'] = inventory.id
2736 product_line['theoretical_qty'] = product_line['product_qty']
2737 if product_line['product_id']:
2738 product = product_obj.browse(cr, uid, product_line['product_id'], context=context)
2739 product_line['product_uom_id'] = product.uom_id.id
2740 vals.append(product_line)
2744 class stock_inventory_line(osv.osv):
2745 _name = "stock.inventory.line"
2746 _description = "Inventory Line"
2747 _order = "inventory_id, location_name, product_code, product_name, prodlot_name"
2749 def _get_product_name_change(self, cr, uid, ids, context=None):
2750 return self.pool.get('stock.inventory.line').search(cr, uid, [('product_id', 'in', ids)], context=context)
2752 def _get_location_change(self, cr, uid, ids, context=None):
2753 return self.pool.get('stock.inventory.line').search(cr, uid, [('location_id', 'in', ids)], context=context)
2755 def _get_prodlot_change(self, cr, uid, ids, context=None):
2756 return self.pool.get('stock.inventory.line').search(cr, uid, [('prod_lot_id', 'in', ids)], context=context)
2759 'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2760 'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
2761 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2762 'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
2763 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
2764 'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
2765 'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
2766 'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
2767 'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
2768 'theoretical_qty': fields.float('Theoretical Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), readonly=True),
2769 'partner_id': fields.many2one('res.partner', 'Owner'),
2770 'product_name': fields.related('product_id', 'name', type='char', string='Product Name', store={
2771 'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
2772 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
2773 'product_code': fields.related('product_id', 'default_code', type='char', string='Product Code', store={
2774 'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
2775 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
2776 'location_name': fields.related('location_id', 'complete_name', type='char', string='Location Name', store={
2777 'stock.location': (_get_location_change, ['name', 'location_id', 'active'], 20),
2778 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id'], 20),}),
2779 'prodlot_name': fields.related('prod_lot_id', 'name', type='char', string='Serial Number Name', store={
2780 'stock.production.lot': (_get_prodlot_change, ['name'], 20),
2781 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['prod_lot_id'], 20),}),
2788 def create(self, cr, uid, values, context=None):
2791 product_obj = self.pool.get('product.product')
2792 if 'product_id' in values and not 'product_uom_id' in values:
2793 values['product_uom_id'] = product_obj.browse(cr, uid, values.get('product_id'), context=context).uom_id.id
2794 return super(stock_inventory_line, self).create(cr, uid, values, context=context)
2796 def _resolve_inventory_line(self, cr, uid, inventory_line, context=None):
2797 stock_move_obj = self.pool.get('stock.move')
2798 diff = inventory_line.theoretical_qty - inventory_line.product_qty
2801 #each theorical_lines where difference between theoretical and checked quantities is not 0 is a line for which we need to create a stock move
2803 'name': _('INV:') + (inventory_line.inventory_id.name or ''),
2804 'product_id': inventory_line.product_id.id,
2805 'product_uom': inventory_line.product_uom_id.id,
2806 'date': inventory_line.inventory_id.date,
2807 'company_id': inventory_line.inventory_id.company_id.id,
2808 'inventory_id': inventory_line.inventory_id.id,
2809 'state': 'confirmed',
2810 'restrict_lot_id': inventory_line.prod_lot_id.id,
2811 'restrict_partner_id': inventory_line.partner_id.id,
2813 inventory_location_id = inventory_line.product_id.property_stock_inventory.id
2815 #found more than expected
2816 vals['location_id'] = inventory_location_id
2817 vals['location_dest_id'] = inventory_line.location_id.id
2818 vals['product_uom_qty'] = -diff
2820 #found less than expected
2821 vals['location_id'] = inventory_line.location_id.id
2822 vals['location_dest_id'] = inventory_location_id
2823 vals['product_uom_qty'] = diff
2824 return stock_move_obj.create(cr, uid, vals, context=context)
2826 def restrict_change(self, cr, uid, ids, theoretical_qty, context=None):
2827 if ids and theoretical_qty:
2828 #if the user try to modify a line prepared by openerp, reject the change and display an error message explaining how he should do
2829 old_value = self.browse(cr, uid, ids[0], context=context)
2832 'product_id': old_value.product_id.id,
2833 'product_uom_id': old_value.product_uom_id.id,
2834 'location_id': old_value.location_id.id,
2835 'prod_lot_id': old_value.prod_lot_id.id,
2836 'package_id': old_value.package_id.id,
2837 'partner_id': old_value.partner_id.id,
2840 'title': _('Error'),
2841 'message': _('You can only change the checked quantity of an existing inventory line. If you want modify a data, please set the checked quantity to 0 and create a new inventory line.')
2846 def on_change_product_id(self, cr, uid, ids, product, uom, theoretical_qty, context=None):
2848 @param location_id: Location id
2849 @param product: Changed product_id
2850 @param uom: UoM product
2851 @return: Dictionary of changed values
2853 if ids and theoretical_qty:
2854 return self.restrict_change(cr, uid, ids, theoretical_qty, context=context)
2856 return {'value': {'product_uom_id': False}}
2857 obj_product = self.pool.get('product.product').browse(cr, uid, product, context=context)
2858 return {'value': {'product_uom_id': uom or obj_product.uom_id.id}}
2861 #----------------------------------------------------------
2863 #----------------------------------------------------------
2864 class stock_warehouse(osv.osv):
2865 _name = "stock.warehouse"
2866 _description = "Warehouse"
2869 'name': fields.char('Warehouse Name', required=True, select=True),
2870 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, select=True),
2871 'partner_id': fields.many2one('res.partner', 'Address'),
2872 'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]),
2873 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', domain=[('usage', '=', 'internal')], required=True),
2874 'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"),
2875 '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'),
2876 'reception_steps': fields.selection([
2877 ('one_step', 'Receive goods directly in stock (1 step)'),
2878 ('two_steps', 'Unload in input location then go to stock (2 steps)'),
2879 ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments',
2880 help="Default incoming route to follow", required=True),
2881 'delivery_steps': fields.selection([
2882 ('ship_only', 'Ship directly from stock (Ship only)'),
2883 ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
2884 ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], 'Outgoing Shippings',
2885 help="Default outgoing route to follow", required=True),
2886 'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'),
2887 'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'),
2888 'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'),
2889 'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'),
2890 'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'),
2891 'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'),
2892 'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'),
2893 'out_type_id': fields.many2one('stock.picking.type', 'Out Type'),
2894 'in_type_id': fields.many2one('stock.picking.type', 'In Type'),
2895 'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'),
2896 'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'),
2897 'reception_route_id': fields.many2one('stock.location.route', 'Receipt Route'),
2898 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'),
2899 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'),
2900 'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'),
2901 'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes',
2902 help="Routes will be created for these resupply warehouses and you can select them on products and product categories"),
2903 'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse', help="Goods will always be resupplied from this warehouse"),
2906 def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None):
2907 resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))])
2908 if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id
2909 resupply_wh_ids.add(default_resupply_wh_id)
2910 resupply_wh_ids = list(resupply_wh_ids)
2911 return {'value': {'resupply_wh_ids': resupply_wh_ids}}
2913 def _get_external_transit_location(self, cr, uid, warehouse, context=None):
2914 ''' returns browse record of inter company transit location, if found'''
2915 data_obj = self.pool.get('ir.model.data')
2916 location_obj = self.pool.get('stock.location')
2918 inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1]
2921 return location_obj.browse(cr, uid, inter_wh_loc, context=context)
2923 def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2925 'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2926 'warehouse_selectable': False,
2927 'product_selectable': True,
2928 'product_categ_selectable': True,
2929 'supplied_wh_id': warehouse.id,
2930 'supplier_wh_id': wh.id,
2933 def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2934 route_obj = self.pool.get('stock.location.route')
2935 pull_obj = self.pool.get('procurement.rule')
2936 #create route selectable on the product to resupply the warehouse from another one
2937 external_transit_location = self._get_external_transit_location(cr, uid, warehouse, context=context)
2938 internal_transit_location = warehouse.company_id.internal_transit_location_id
2939 input_loc = warehouse.wh_input_stock_loc_id
2940 if warehouse.reception_steps == 'one_step':
2941 input_loc = warehouse.lot_stock_id
2942 for wh in supplier_warehouses:
2943 transit_location = wh.company_id.id == warehouse.company_id.id and internal_transit_location or external_transit_location
2944 if transit_location:
2945 output_loc = wh.wh_output_stock_loc_id
2946 if wh.delivery_steps == 'ship_only':
2947 output_loc = wh.lot_stock_id
2948 # Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists)
2949 mto_pull_vals = self._get_mto_pull_rule(cr, uid, wh, [(output_loc, transit_location, wh.out_type_id.id)], context=context)[0]
2950 pull_obj.create(cr, uid, mto_pull_vals, context=context)
2951 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2952 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2953 values = [(output_loc, transit_location, wh.out_type_id.id, wh), (transit_location, input_loc, warehouse.in_type_id.id, warehouse)]
2954 pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context)
2955 for pull_rule in pull_rules_list:
2956 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2957 #if the warehouse is also set as default resupply method, assign this route automatically to the warehouse
2958 if default_resupply_wh and default_resupply_wh.id == wh.id:
2959 self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2962 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2963 'reception_steps': 'one_step',
2964 'delivery_steps': 'ship_only',
2966 _sql_constraints = [
2967 ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2968 ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2971 def _get_partner_locations(self, cr, uid, ids, context=None):
2972 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2973 data_obj = self.pool.get('ir.model.data')
2974 location_obj = self.pool.get('stock.location')
2976 customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2977 supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2979 customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2980 customer_loc = customer_loc and customer_loc[0] or False
2981 supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2982 supplier_loc = supplier_loc and supplier_loc[0] or False
2983 if not (customer_loc and supplier_loc):
2984 raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2985 return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2987 def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2988 location_obj = self.pool.get('stock.location')
2990 new_reception_step = new_reception_step or warehouse.reception_steps
2991 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2992 if warehouse.reception_steps != new_reception_step:
2993 location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2994 if new_reception_step != 'one_step':
2995 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2996 if new_reception_step == 'three_steps':
2997 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2999 if warehouse.delivery_steps != new_delivery_step:
3000 location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
3001 if new_delivery_step != 'ship_only':
3002 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
3003 if new_delivery_step == 'pick_pack_ship':
3004 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
3007 def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
3009 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
3010 'product_categ_selectable': True,
3011 'product_selectable': False,
3015 def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None):
3016 pull_rules_list = []
3017 for from_loc, dest_loc, pick_type_id, warehouse in values:
3018 pull_rules_list.append({
3019 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
3020 'location_src_id': from_loc.id,
3021 'location_id': dest_loc.id,
3022 'route_id': new_route_id,
3024 'picking_type_id': pick_type_id,
3025 'procure_method': warehouse.lot_stock_id.id != from_loc.id and 'make_to_order' or 'make_to_stock', # first part of the resuply route is MTS
3026 'warehouse_id': supplied_warehouse.id,
3027 'propagate_warehouse_id': warehouse.id,
3029 return pull_rules_list
3031 def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
3033 push_rules_list = []
3034 pull_rules_list = []
3035 for from_loc, dest_loc, pick_type_id in values:
3036 push_rules_list.append({
3037 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
3038 'location_from_id': from_loc.id,
3039 'location_dest_id': dest_loc.id,
3040 'route_id': new_route_id,
3042 'picking_type_id': pick_type_id,
3044 'warehouse_id': warehouse.id,
3046 pull_rules_list.append({
3047 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
3048 'location_src_id': from_loc.id,
3049 'location_id': dest_loc.id,
3050 'route_id': new_route_id,
3052 'picking_type_id': pick_type_id,
3053 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
3055 'warehouse_id': warehouse.id,
3058 return push_rules_list, pull_rules_list
3060 def _get_mto_route(self, cr, uid, context=None):
3061 route_obj = self.pool.get('stock.location.route')
3062 data_obj = self.pool.get('ir.model.data')
3064 mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
3066 mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('Make To Order'))], context=context)
3067 mto_route_id = mto_route_id and mto_route_id[0] or False
3068 if not mto_route_id:
3069 raise osv.except_osv(_('Error!'), _('Can\'t find any generic Make To Order route.'))
3072 def _check_remove_mto_resupply_rules(self, cr, uid, warehouse, context=None):
3073 """ Checks that the moves from the different """
3074 pull_obj = self.pool.get('procurement.rule')
3075 mto_route_id = self._get_mto_route(cr, uid, context=context)
3076 rules = pull_obj.search(cr, uid, ['&', ('location_src_id', '=', warehouse.lot_stock_id.id), ('location_id.usage', '=', 'transit')], context=context)
3077 pull_obj.unlink(cr, uid, rules, context=context)
3079 def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
3080 mto_route_id = self._get_mto_route(cr, uid, context=context)
3082 for value in values:
3083 from_loc, dest_loc, pick_type_id = value
3085 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
3086 'location_src_id': from_loc.id,
3087 'location_id': dest_loc.id,
3088 'route_id': mto_route_id,
3090 'picking_type_id': pick_type_id,
3091 'procure_method': 'make_to_order',
3093 'warehouse_id': warehouse.id,
3097 def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
3099 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
3100 'warehouse_selectable': False,
3101 'product_selectable': True,
3102 'product_categ_selectable': True,
3103 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
3107 def create_routes(self, cr, uid, ids, warehouse, context=None):
3109 route_obj = self.pool.get('stock.location.route')
3110 pull_obj = self.pool.get('procurement.rule')
3111 push_obj = self.pool.get('stock.location.path')
3112 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
3113 #create reception route and rules
3114 route_name, values = routes_dict[warehouse.reception_steps]
3115 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
3116 reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
3117 wh_route_ids.append((4, reception_route_id))
3118 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
3119 #create the push/pull rules
3120 for push_rule in push_rules_list:
3121 push_obj.create(cr, uid, vals=push_rule, context=context)
3122 for pull_rule in pull_rules_list:
3123 #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
3124 pull_rule['procure_method'] = 'make_to_order'
3125 pull_obj.create(cr, uid, vals=pull_rule, context=context)
3127 #create MTS route and pull rules for delivery and a specific route MTO to be set on the product
3128 route_name, values = routes_dict[warehouse.delivery_steps]
3129 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
3130 #create the route and its pull rules
3131 delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
3132 wh_route_ids.append((4, delivery_route_id))
3133 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
3134 for pull_rule in pull_rules_list:
3135 pull_obj.create(cr, uid, vals=pull_rule, context=context)
3136 #create MTO pull rule and link it to the generic MTO route
3137 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)[0]
3138 mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
3140 #create a route for cross dock operations, that can be set on products and product categories
3141 route_name, values = routes_dict['crossdock']
3142 crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
3143 crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
3144 wh_route_ids.append((4, crossdock_route_id))
3145 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)
3146 for pull_rule in pull_rules_list:
3147 # Fixed cross-dock is logically mto
3148 pull_rule['procure_method'] = 'make_to_order'
3149 pull_obj.create(cr, uid, vals=pull_rule, context=context)
3151 #create route selectable on the product to resupply the warehouse from another one
3152 self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
3154 #return routes and mto pull rule to store on the warehouse
3156 'route_ids': wh_route_ids,
3157 'mto_pull_id': mto_pull_id,
3158 'reception_route_id': reception_route_id,
3159 'delivery_route_id': delivery_route_id,
3160 'crossdock_route_id': crossdock_route_id,
3163 def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
3164 picking_type_obj = self.pool.get('stock.picking.type')
3165 pull_obj = self.pool.get('procurement.rule')
3166 push_obj = self.pool.get('stock.location.path')
3167 route_obj = self.pool.get('stock.location.route')
3168 new_reception_step = new_reception_step or warehouse.reception_steps
3169 new_delivery_step = new_delivery_step or warehouse.delivery_steps
3171 #change the default source and destination location and (de)activate picking types
3172 input_loc = warehouse.wh_input_stock_loc_id
3173 if new_reception_step == 'one_step':
3174 input_loc = warehouse.lot_stock_id
3175 output_loc = warehouse.wh_output_stock_loc_id
3176 if new_delivery_step == 'ship_only':
3177 output_loc = warehouse.lot_stock_id
3178 picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
3179 picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
3180 picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
3181 picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
3183 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
3184 #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
3185 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
3186 route_name, values = routes_dict[new_delivery_step]
3187 route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
3188 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
3189 #create the pull rules
3190 for pull_rule in pull_rules_list:
3191 pull_obj.create(cr, uid, vals=pull_rule, context=context)
3193 #update receipt route and rules: unlink the existing rules of the warehouse receipt route and recreate it
3194 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
3195 push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
3196 route_name, values = routes_dict[new_reception_step]
3197 route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
3198 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
3199 #create the push/pull rules
3200 for push_rule in push_rules_list:
3201 push_obj.create(cr, uid, vals=push_rule, context=context)
3202 for pull_rule in pull_rules_list:
3203 #all pull rules in receipt route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location
3204 pull_rule['procure_method'] = 'make_to_order'
3205 pull_obj.create(cr, uid, vals=pull_rule, context=context)
3207 route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
3210 dummy, values = routes_dict[new_delivery_step]
3211 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)[0]
3212 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
3215 def create_sequences_and_picking_types(self, cr, uid, warehouse, context=None):
3216 seq_obj = self.pool.get('ir.sequence')
3217 picking_type_obj = self.pool.get('stock.picking.type')
3218 #create new sequences
3219 in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence in'), 'prefix': warehouse.code + '/IN/', 'padding': 5}, context=context)
3220 out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence out'), 'prefix': warehouse.code + '/OUT/', 'padding': 5}, context=context)
3221 pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence packing'), 'prefix': warehouse.code + '/PACK/', 'padding': 5}, context=context)
3222 pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence picking'), 'prefix': warehouse.code + '/PICK/', 'padding': 5}, context=context)
3223 int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence internal'), 'prefix': warehouse.code + '/INT/', 'padding': 5}, context=context)
3225 wh_stock_loc = warehouse.lot_stock_id
3226 wh_input_stock_loc = warehouse.wh_input_stock_loc_id
3227 wh_output_stock_loc = warehouse.wh_output_stock_loc_id
3228 wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
3230 #fetch customer and supplier locations, for references
3231 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, warehouse.id, context=context)
3233 #create in, out, internal picking types for warehouse
3234 input_loc = wh_input_stock_loc
3235 if warehouse.reception_steps == 'one_step':
3236 input_loc = wh_stock_loc
3237 output_loc = wh_output_stock_loc
3238 if warehouse.delivery_steps == 'ship_only':
3239 output_loc = wh_stock_loc
3241 #choose the next available color for the picking types of this warehouse
3243 available_colors = [c%9 for c in range(3, 12)] # put flashy colors first
3244 all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
3245 #don't use sets to preserve the list order
3246 for x in all_used_colors:
3247 if x['color'] in available_colors:
3248 available_colors.remove(x['color'])
3249 if available_colors:
3250 color = available_colors[0]
3252 #order the picking types with a sequence allowing to have the following suit for each warehouse: reception, internal, pick, pack, ship.
3253 max_sequence = self.pool.get('stock.picking.type').search_read(cr, uid, [], ['sequence'], order='sequence desc')
3254 max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
3256 in_type_id = picking_type_obj.create(cr, uid, vals={
3257 'name': _('Receipts'),
3258 'warehouse_id': warehouse.id,
3260 'sequence_id': in_seq_id,
3261 'default_location_src_id': supplier_loc.id,
3262 'default_location_dest_id': input_loc.id,
3263 'sequence': max_sequence + 1,
3264 'color': color}, context=context)
3265 out_type_id = picking_type_obj.create(cr, uid, vals={
3266 'name': _('Delivery Orders'),
3267 'warehouse_id': warehouse.id,
3269 'sequence_id': out_seq_id,
3270 'return_picking_type_id': in_type_id,
3271 'default_location_src_id': output_loc.id,
3272 'default_location_dest_id': customer_loc.id,
3273 'sequence': max_sequence + 4,
3274 'color': color}, context=context)
3275 picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context)
3276 int_type_id = picking_type_obj.create(cr, uid, vals={
3277 'name': _('Internal Transfers'),
3278 'warehouse_id': warehouse.id,
3280 'sequence_id': int_seq_id,
3281 'default_location_src_id': wh_stock_loc.id,
3282 'default_location_dest_id': wh_stock_loc.id,
3284 'sequence': max_sequence + 2,
3285 'color': color}, context=context)
3286 pack_type_id = picking_type_obj.create(cr, uid, vals={
3288 'warehouse_id': warehouse.id,
3290 'sequence_id': pack_seq_id,
3291 'default_location_src_id': wh_pack_stock_loc.id,
3292 'default_location_dest_id': output_loc.id,
3293 'active': warehouse.delivery_steps == 'pick_pack_ship',
3294 'sequence': max_sequence + 3,
3295 'color': color}, context=context)
3296 pick_type_id = picking_type_obj.create(cr, uid, vals={
3298 'warehouse_id': warehouse.id,
3300 'sequence_id': pick_seq_id,
3301 'default_location_src_id': wh_stock_loc.id,
3302 'default_location_dest_id': wh_pack_stock_loc.id,
3303 'active': warehouse.delivery_steps != 'ship_only',
3304 'sequence': max_sequence + 2,
3305 'color': color}, context=context)
3307 #write picking types on WH
3309 'in_type_id': in_type_id,
3310 'out_type_id': out_type_id,
3311 'pack_type_id': pack_type_id,
3312 'pick_type_id': pick_type_id,
3313 'int_type_id': int_type_id,
3315 super(stock_warehouse, self).write(cr, uid, warehouse.id, vals=vals, context=context)
3318 def create(self, cr, uid, vals, context=None):
3323 data_obj = self.pool.get('ir.model.data')
3324 seq_obj = self.pool.get('ir.sequence')
3325 picking_type_obj = self.pool.get('stock.picking.type')
3326 location_obj = self.pool.get('stock.location')
3328 #create view location for warehouse
3330 'name': _(vals.get('code')),
3332 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1],
3334 if vals.get('company_id'):
3335 loc_vals['company_id'] = vals.get('company_id')
3336 wh_loc_id = location_obj.create(cr, uid, loc_vals, context=context)
3337 vals['view_location_id'] = wh_loc_id
3338 #create all location
3339 def_values = self.default_get(cr, uid, {'reception_steps', 'delivery_steps'})
3340 reception_steps = vals.get('reception_steps', def_values['reception_steps'])
3341 delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
3342 context_with_inactive = context.copy()
3343 context_with_inactive['active_test'] = False
3345 {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
3346 {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
3347 {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
3348 {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
3349 {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
3351 for values in sub_locations:
3353 'name': values['name'],
3354 'usage': 'internal',
3355 'location_id': wh_loc_id,
3356 'active': values['active'],
3358 if vals.get('company_id'):
3359 loc_vals['company_id'] = vals.get('company_id')
3360 location_id = location_obj.create(cr, uid, loc_vals, context=context_with_inactive)
3361 vals[values['field']] = location_id
3364 new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
3365 warehouse = self.browse(cr, uid, new_id, context=context)
3366 self.create_sequences_and_picking_types(cr, uid, warehouse, context=context)
3368 #create routes and push/pull rules
3369 new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
3370 self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
3373 def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
3374 return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name
3376 def _format_routename(self, cr, uid, obj, name, context=None):
3377 return obj.name + ': ' + name
3379 def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
3380 #fetch customer and supplier locations, for references
3381 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
3384 'one_step': (_('Receipt in 1 step'), []),
3385 'two_steps': (_('Receipt in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
3386 'three_steps': (_('Receipt 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)]),
3387 '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)]),
3388 'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
3389 '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)]),
3390 'pick_pack_ship': (_('Pick + Pack + Ship'), [(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_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)]),
3393 def _handle_renaming(self, cr, uid, warehouse, name, code, context=None):
3394 location_obj = self.pool.get('stock.location')
3395 route_obj = self.pool.get('stock.location.route')
3396 pull_obj = self.pool.get('procurement.rule')
3397 push_obj = self.pool.get('stock.location.path')
3399 location_id = warehouse.lot_stock_id.location_id.id
3400 location_obj.write(cr, uid, location_id, {'name': code}, context=context)
3401 #rename route and push-pull rules
3402 for route in warehouse.route_ids:
3403 route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context)
3404 for pull in route.pull_ids:
3405 pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
3406 for push in route.push_ids:
3407 push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
3408 #change the mto pull rule name
3409 if warehouse.mto_pull_id.id:
3410 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
3412 def _check_delivery_resupply(self, cr, uid, warehouse, new_location, change_to_multiple, context=None):
3413 """ Will check if the resupply routes from this warehouse follow the changes of number of delivery steps """
3414 #Check routes that are being delivered by this warehouse and change the rule going to transit location
3415 route_obj = self.pool.get("stock.location.route")
3416 pull_obj = self.pool.get("procurement.rule")
3417 routes = route_obj.search(cr, uid, [('supplier_wh_id','=', warehouse.id)], context=context)
3418 pulls = pull_obj.search(cr, uid, ['&', ('route_id', 'in', routes), ('location_id.usage', '=', 'transit')], context=context)
3420 pull_obj.write(cr, uid, pulls, {'location_src_id': new_location, 'procure_method': change_to_multiple and "make_to_order" or "make_to_stock"}, context=context)
3421 # Create or clean MTO rules
3422 mto_route_id = self._get_mto_route(cr, uid, context=context)
3423 if not change_to_multiple:
3424 # If single delivery we should create the necessary MTO rules for the resupply
3425 # pulls = pull_obj.search(cr, uid, ['&', ('route_id', '=', mto_route_id), ('location_id.usage', '=', 'transit'), ('location_src_id', '=', warehouse.lot_stock_id.id)], context=context)
3426 pull_recs = pull_obj.browse(cr, uid, pulls, context=context)
3427 transfer_locs = list(set([x.location_id for x in pull_recs]))
3428 vals = [(warehouse.lot_stock_id , x, warehouse.out_type_id.id) for x in transfer_locs]
3429 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, vals, context=context)
3430 for mto_pull_val in mto_pull_vals:
3431 pull_obj.create(cr, uid, mto_pull_val, context=context)
3433 # We need to delete all the MTO pull rules, otherwise they risk to be used in the system
3434 pulls = pull_obj.search(cr, uid, ['&', ('route_id', '=', mto_route_id), ('location_id.usage', '=', 'transit'), ('location_src_id', '=', warehouse.lot_stock_id.id)], context=context)
3436 pull_obj.unlink(cr, uid, pulls, context=context)
3438 def _check_reception_resupply(self, cr, uid, warehouse, new_location, context=None):
3440 Will check if the resupply routes to this warehouse follow the changes of number of receipt steps
3442 #Check routes that are being delivered by this warehouse and change the rule coming from transit location
3443 route_obj = self.pool.get("stock.location.route")
3444 pull_obj = self.pool.get("procurement.rule")
3445 routes = route_obj.search(cr, uid, [('supplied_wh_id','=', warehouse.id)], context=context)
3446 pulls= pull_obj.search(cr, uid, ['&', ('route_id', 'in', routes), ('location_src_id.usage', '=', 'transit')])
3448 pull_obj.write(cr, uid, pulls, {'location_id': new_location}, context=context)
3450 def _check_resupply(self, cr, uid, warehouse, reception_new, delivery_new, context=None):
3452 old_val = warehouse.reception_steps
3453 new_val = reception_new
3454 change_to_one = (old_val != 'one_step' and new_val == 'one_step')
3455 change_to_multiple = (old_val == 'one_step' and new_val != 'one_step')
3456 if change_to_one or change_to_multiple:
3457 new_location = change_to_one and warehouse.lot_stock_id.id or warehouse.wh_input_stock_loc_id.id
3458 self._check_reception_resupply(cr, uid, warehouse, new_location, context=context)
3460 old_val = warehouse.delivery_steps
3461 new_val = delivery_new
3462 change_to_one = (old_val != 'ship_only' and new_val == 'ship_only')
3463 change_to_multiple = (old_val == 'ship_only' and new_val != 'ship_only')
3464 if change_to_one or change_to_multiple:
3465 new_location = change_to_one and warehouse.lot_stock_id.id or warehouse.wh_output_stock_loc_id.id
3466 self._check_delivery_resupply(cr, uid, warehouse, new_location, change_to_multiple, context=context)
3468 def write(self, cr, uid, ids, vals, context=None):
3471 if isinstance(ids, (int, long)):
3473 seq_obj = self.pool.get('ir.sequence')
3474 route_obj = self.pool.get('stock.location.route')
3475 context_with_inactive = context.copy()
3476 context_with_inactive['active_test'] = False
3477 for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
3478 #first of all, check if we need to delete and recreate route
3479 if vals.get('reception_steps') or vals.get('delivery_steps'):
3480 #activate and deactivate location according to reception and delivery option
3481 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
3482 # switch between route
3483 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
3484 # Check if we need to change something to resupply warehouses and associated MTO rules
3485 self._check_resupply(cr, uid, warehouse, vals.get('reception_steps'), vals.get('delivery_steps'), context=context)
3486 if vals.get('code') or vals.get('name'):
3487 name = warehouse.name
3489 if vals.get('name'):
3490 name = vals.get('name', warehouse.name)
3491 self._handle_renaming(cr, uid, warehouse, name, vals.get('code', warehouse.code), context=context_with_inactive)
3492 if warehouse.in_type_id:
3493 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)
3494 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)
3495 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)
3496 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)
3497 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)
3498 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
3499 for cmd in vals.get('resupply_wh_ids'):
3501 new_ids = set(cmd[2])
3502 old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
3503 to_add_wh_ids = new_ids - old_ids
3505 supplier_warehouses = self.browse(cr, uid, list(to_add_wh_ids), context=context)
3506 self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
3507 to_remove_wh_ids = old_ids - new_ids
3508 if to_remove_wh_ids:
3509 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)
3510 if to_remove_route_ids:
3511 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
3515 if 'default_resupply_wh_id' in vals:
3516 if vals.get('default_resupply_wh_id') == warehouse.id:
3517 raise osv.except_osv(_('Warning'),_('The default resupply warehouse should be different than the warehouse itself!'))
3518 if warehouse.default_resupply_wh_id:
3519 #remove the existing resupplying route on the warehouse
3520 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)
3521 for inter_wh_route_id in to_remove_route_ids:
3522 self.write(cr, uid, [warehouse.id], {'route_ids': [(3, inter_wh_route_id)]})
3523 if vals.get('default_resupply_wh_id'):
3524 #assign the new resupplying route on all products
3525 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)
3526 for inter_wh_route_id in to_assign_route_ids:
3527 self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]})
3529 return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
3531 def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
3532 route_obj = self.pool.get("stock.location.route")
3533 all_routes = [route.id for route in warehouse.route_ids]
3534 all_routes += route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id)], context=context)
3535 all_routes += [warehouse.mto_pull_id.route_id.id]
3538 def view_all_routes_for_wh(self, cr, uid, ids, context=None):
3540 for wh in self.browse(cr, uid, ids, context=context):
3541 all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
3543 domain = [('id', 'in', all_routes)]
3545 'name': _('Warehouse\'s Routes'),
3547 'res_model': 'stock.location.route',
3548 'type': 'ir.actions.act_window',
3550 'view_mode': 'tree,form',
3551 'view_type': 'form',
3556 class stock_location_path(osv.osv):
3557 _name = "stock.location.path"
3558 _description = "Pushed Flows"
3561 def _get_rules(self, cr, uid, ids, context=None):
3563 for route in self.browse(cr, uid, ids, context=context):
3564 res += [x.id for x in route.push_ids]
3568 'name': fields.char('Operation Name', required=True),
3569 'company_id': fields.many2one('res.company', 'Company'),
3570 'route_id': fields.many2one('stock.location.route', 'Route'),
3571 'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
3572 'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
3573 'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
3574 '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"),
3575 'auto': fields.selection(
3576 [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
3578 required=True, select=1,
3579 help="This is used to define paths the product has to follow within the location tree.\n" \
3580 "The 'Automatic Move' value will create a stock move after the current one that will be "\
3581 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
3582 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
3584 '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'),
3585 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the rule without removing it."),
3586 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
3587 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence',
3589 'stock.location.route': (_get_rules, ['sequence'], 10),
3590 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10),
3592 'sequence': fields.integer('Sequence'),
3597 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
3602 def _prepare_push_apply(self, cr, uid, rule, move, context=None):
3603 newdate = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta.relativedelta(days=rule.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
3605 'location_id': move.location_dest_id.id,
3606 'location_dest_id': rule.location_dest_id.id,
3608 'company_id': rule.company_id and rule.company_id.id or False,
3609 'date_expected': newdate,
3610 'picking_id': False,
3611 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
3612 'propagate': rule.propagate,
3613 'push_rule_id': rule.id,
3614 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
3617 def _apply(self, cr, uid, rule, move, context=None):
3618 move_obj = self.pool.get('stock.move')
3619 newdate = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta.relativedelta(days=rule.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
3620 if rule.auto == 'transparent':
3621 old_dest_location = move.location_dest_id.id
3622 move_obj.write(cr, uid, [move.id], {
3624 'date_expected': newdate,
3625 'location_dest_id': rule.location_dest_id.id
3627 #avoid looping if a push rule is not well configured
3628 if rule.location_dest_id.id != old_dest_location:
3629 #call again push_apply to see if a next step is defined
3630 move_obj._push_apply(cr, uid, [move], context=context)
3632 vals = self._prepare_push_apply(cr, uid, rule, move, context=context)
3633 move_id = move_obj.copy(cr, uid, move.id, vals, context=context)
3634 move_obj.write(cr, uid, [move.id], {
3635 'move_dest_id': move_id,
3637 move_obj.action_confirm(cr, uid, [move_id], context=None)
3640 # -------------------------
3641 # Packaging related stuff
3642 # -------------------------
3644 from openerp.report import report_sxw
3646 class stock_package(osv.osv):
3648 These are the packages, containing quants and/or other packages
3650 _name = "stock.quant.package"
3651 _description = "Physical Packages"
3652 _parent_name = "parent_id"
3653 _parent_store = True
3654 _parent_order = 'name'
3655 _order = 'parent_left'
3657 def name_get(self, cr, uid, ids, context=None):
3658 res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
3661 def _complete_name(self, cr, uid, ids, name, args, context=None):
3662 """ Forms complete name of location from parent location to child location.
3663 @return: Dictionary of values
3666 for m in self.browse(cr, uid, ids, context=context):
3668 parent = m.parent_id
3670 res[m.id] = parent.name + ' / ' + res[m.id]
3671 parent = parent.parent_id
3674 def _get_packages(self, cr, uid, ids, context=None):
3675 """Returns packages from quants for store"""
3677 for quant in self.browse(cr, uid, ids, context=context):
3678 if quant.package_id:
3679 res.add(quant.package_id.id)
3682 def _get_packages_to_relocate(self, cr, uid, ids, context=None):
3684 for pack in self.browse(cr, uid, ids, context=context):
3687 res.add(pack.parent_id.id)
3690 def _get_package_info(self, cr, uid, ids, name, args, context=None):
3691 default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
3692 res = dict((res_id, {'location_id': False, 'company_id': default_company_id, 'owner_id': False}) for res_id in ids)
3693 for pack in self.browse(cr, uid, ids, context=context):
3695 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
3696 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
3697 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
3698 elif pack.children_ids:
3699 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
3700 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
3701 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
3705 'name': fields.char('Package Reference', select=True, copy=False),
3706 'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
3707 'parent_left': fields.integer('Left Parent', select=1),
3708 'parent_right': fields.integer('Right Parent', select=1),
3709 'packaging_id': fields.many2one('product.packaging', 'Packaging', help="This field should be completed only if everything inside the package share the same product, otherwise it doesn't really makes sense.", select=True),
3710 'ul_id': fields.many2one('product.ul', 'Logistic Unit'),
3711 'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
3713 'stock.quant': (_get_packages, ['location_id'], 10),
3714 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3715 }, readonly=True, select=True),
3716 'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content', readonly=True),
3717 'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict', readonly=True),
3718 'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages', readonly=True),
3719 'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package",
3721 'stock.quant': (_get_packages, ['company_id'], 10),
3722 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3723 }, readonly=True, select=True),
3724 'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
3726 'stock.quant': (_get_packages, ['owner_id'], 10),
3727 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3728 }, readonly=True, select=True),
3731 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').next_by_code(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3734 def _check_location_constraint(self, cr, uid, packs, context=None):
3735 '''checks that all quants in a package are stored in the same location. This function cannot be used
3736 as a constraint because it needs to be checked on pack operations (they may not call write on the
3739 quant_obj = self.pool.get('stock.quant')
3742 while parent.parent_id:
3743 parent = parent.parent_id
3744 quant_ids = self.get_content(cr, uid, [parent.id], context=context)
3745 quants = [x for x in quant_obj.browse(cr, uid, quant_ids, context=context) if x.qty > 0]
3746 location_id = quants and quants[0].location_id.id or False
3747 if not [quant.location_id.id == location_id for quant in quants]:
3748 raise osv.except_osv(_('Error'), _('Everything inside a package should be in the same location'))
3751 def action_print(self, cr, uid, ids, context=None):
3752 context = dict(context or {}, active_ids=ids)
3753 return self.pool.get("report").get_action(cr, uid, ids, 'stock.report_package_barcode_small', context=context)
3756 def unpack(self, cr, uid, ids, context=None):
3757 quant_obj = self.pool.get('stock.quant')
3758 for package in self.browse(cr, uid, ids, context=context):
3759 quant_ids = [quant.id for quant in package.quant_ids]
3760 quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3761 children_package_ids = [child_package.id for child_package in package.children_ids]
3762 self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3763 #delete current package since it contains nothing anymore
3764 self.unlink(cr, uid, ids, context=context)
3765 return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3767 def get_content(self, cr, uid, ids, context=None):
3768 child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3769 return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3771 def get_content_package(self, cr, uid, ids, context=None):
3772 quants_ids = self.get_content(cr, uid, ids, context=context)
3773 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3774 res['domain'] = [('id', 'in', quants_ids)]
3777 def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3778 ''' find the total of given product 'product_id' inside the given package 'package_id'''
3779 quant_obj = self.pool.get('stock.quant')
3780 all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3782 for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3783 if quant.product_id.id == product_id:
3787 def _get_all_products_quantities(self, cr, uid, package_id, context=None):
3788 '''This function computes the different product quantities for the given package
3790 quant_obj = self.pool.get('stock.quant')
3792 for quant in quant_obj.browse(cr, uid, self.get_content(cr, uid, package_id, context=context)):
3793 if quant.product_id.id not in res:
3794 res[quant.product_id.id] = 0
3795 res[quant.product_id.id] += quant.qty
3798 def copy_pack(self, cr, uid, id, default_pack_values=None, default=None, context=None):
3799 stock_pack_operation_obj = self.pool.get('stock.pack.operation')
3802 new_package_id = self.copy(cr, uid, id, default_pack_values, context=context)
3803 default['result_package_id'] = new_package_id
3804 op_ids = stock_pack_operation_obj.search(cr, uid, [('result_package_id', '=', id)], context=context)
3805 for op_id in op_ids:
3806 stock_pack_operation_obj.copy(cr, uid, op_id, default, context=context)
3809 class stock_pack_operation(osv.osv):
3810 _name = "stock.pack.operation"
3811 _description = "Packing Operation"
3813 def _get_remaining_prod_quantities(self, cr, uid, operation, context=None):
3814 '''Get the remaining quantities per product on an operation with a package. This function returns a dictionary'''
3815 #if the operation doesn't concern a package, it's not relevant to call this function
3816 if not operation.package_id or operation.product_id:
3817 return {operation.product_id.id: operation.remaining_qty}
3818 #get the total of products the package contains
3819 res = self.pool.get('stock.quant.package')._get_all_products_quantities(cr, uid, operation.package_id.id, context=context)
3820 #reduce by the quantities linked to a move
3821 for record in operation.linked_move_operation_ids:
3822 if record.move_id.product_id.id not in res:
3823 res[record.move_id.product_id.id] = 0
3824 res[record.move_id.product_id.id] -= record.qty
3827 def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3828 uom_obj = self.pool.get('product.uom')
3830 for ops in self.browse(cr, uid, ids, context=context):
3832 if ops.package_id and not ops.product_id:
3833 #dont try to compute the remaining quantity for packages because it's not relevant (a package could include different products).
3834 #should use _get_remaining_prod_quantities instead
3837 qty = ops.product_qty
3838 if ops.product_uom_id:
3839 qty = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
3840 for record in ops.linked_move_operation_ids:
3842 res[ops.id] = float_round(qty, precision_rounding=ops.product_id.uom_id.rounding)
3845 def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3846 res = self.on_change_tests(cr, uid, ids, product_id, product_uom_id, product_qty, context=context)
3847 if product_id and not product_uom_id:
3848 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3849 res['value']['product_uom_id'] = product.uom_id.id
3852 def on_change_tests(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3854 uom_obj = self.pool.get('product.uom')
3856 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3857 product_uom_id = product_uom_id or product.uom_id.id
3858 selected_uom = uom_obj.browse(cr, uid, product_uom_id, context=context)
3859 if selected_uom.category_id.id != product.uom_id.category_id.id:
3861 'title': _('Warning: wrong UoM!'),
3862 '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)
3864 if product_qty and 'warning' not in res:
3865 rounded_qty = uom_obj._compute_qty(cr, uid, product_uom_id, product_qty, product_uom_id, round=True)
3866 if rounded_qty != product_qty:
3868 'title': _('Warning: wrong quantity!'),
3869 'message': _('The chosen quantity for product %s is not compatible with the UoM rounding. It will be automatically converted at confirmation') % (product.name)
3874 'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3875 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
3876 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3877 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3878 'qty_done': fields.float('Quantity Processed', digits_compute=dp.get_precision('Product Unit of Measure')),
3879 'package_id': fields.many2one('stock.quant.package', 'Source Package'), # 2
3880 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3881 'result_package_id': fields.many2one('stock.quant.package', 'Destination Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3882 'date': fields.datetime('Date', required=True),
3883 'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3884 #'update_cost': fields.boolean('Need cost update'),
3885 'cost': fields.float("Cost", help="Unit Cost for this product line"),
3886 'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3887 'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'operation_id', string='Linked Moves', readonly=True, help='Moves impacted by this operation for the computation of the remaining quantities'),
3888 'remaining_qty': fields.function(_get_remaining_qty, type='float', digits = 0, string="Remaining Qty", help="Remaining quantity in default UoM according to moves matched with this operation. "),
3889 'location_id': fields.many2one('stock.location', 'Source Location', required=True),
3890 'location_dest_id': fields.many2one('stock.location', 'Destination Location', required=True),
3891 'processed': fields.selection([('true','Yes'), ('false','No')],'Has been processed?', required=True),
3895 'date': fields.date.context_today,
3897 'processed': lambda *a: 'false',
3900 def write(self, cr, uid, ids, vals, context=None):
3901 context = context or {}
3902 res = super(stock_pack_operation, self).write(cr, uid, ids, vals, context=context)
3903 if isinstance(ids, (int, long)):
3905 if not context.get("no_recompute"):
3906 pickings = vals.get('picking_id') and [vals['picking_id']] or list(set([x.picking_id.id for x in self.browse(cr, uid, ids, context=context)]))
3907 self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, pickings, context=context)
3910 def create(self, cr, uid, vals, context=None):
3911 context = context or {}
3912 res_id = super(stock_pack_operation, self).create(cr, uid, vals, context=context)
3913 if vals.get("picking_id") and not context.get("no_recompute"):
3914 self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, [vals['picking_id']], context=context)
3917 def action_drop_down(self, cr, uid, ids, context=None):
3918 ''' Used by barcode interface to say that pack_operation has been moved from src location
3919 to destination location, if qty_done is less than product_qty than we have to split the
3920 operation in two to process the one with the qty moved
3923 move_obj = self.pool.get("stock.move")
3924 for pack_op in self.browse(cr, uid, ids, context=None):
3925 if pack_op.product_id and pack_op.location_id and pack_op.location_dest_id:
3926 move_obj.check_tracking_product(cr, uid, pack_op.product_id, pack_op.lot_id.id, pack_op.location_id, pack_op.location_dest_id, context=context)
3928 if pack_op.qty_done < pack_op.product_qty:
3929 # we split the operation in two
3930 op = self.copy(cr, uid, pack_op.id, {'product_qty': pack_op.qty_done, 'qty_done': pack_op.qty_done}, context=context)
3931 self.write(cr, uid, [pack_op.id], {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0, 'lot_id': False}, context=context)
3932 processed_ids.append(op)
3933 self.write(cr, uid, processed_ids, {'processed': 'true'}, context=context)
3935 def create_and_assign_lot(self, cr, uid, id, name, context=None):
3936 ''' Used by barcode interface to create a new lot and assign it to the operation
3938 obj = self.browse(cr,uid,id,context)
3939 product_id = obj.product_id.id
3940 val = {'product_id': product_id}
3943 lots = self.pool.get('stock.production.lot').search(cr, uid, ['&', ('name', '=', name), ('product_id', '=', product_id)], context=context)
3945 new_lot_id = lots[0]
3946 val.update({'name': name})
3949 new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, val, context=context)
3950 self.write(cr, uid, id, {'lot_id': new_lot_id}, context=context)
3952 def _search_and_increment(self, cr, uid, picking_id, domain, filter_visible=False, visible_op_ids=False, increment=True, context=None):
3953 '''Search for an operation with given 'domain' in a picking, if it exists increment the qty (+1) otherwise create it
3955 :param domain: list of tuple directly reusable as a domain
3956 context can receive a key 'current_package_id' with the package to consider for this operation
3962 #if current_package_id is given in the context, we increase the number of items in this package
3963 package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3964 existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id)] + domain + package_clause, context=context)
3965 todo_operation_ids = []
3966 if existing_operation_ids:
3968 todo_operation_ids = [val for val in existing_operation_ids if val in visible_op_ids]
3970 todo_operation_ids = existing_operation_ids
3971 if todo_operation_ids:
3972 #existing operation found for the given domain and picking => increment its quantity
3973 operation_id = todo_operation_ids[0]
3974 op_obj = self.browse(cr, uid, operation_id, context=context)
3975 qty = op_obj.qty_done
3979 qty -= 1 if qty >= 1 else 0
3980 if qty == 0 and op_obj.product_qty == 0:
3981 #we have a line with 0 qty set, so delete it
3982 self.unlink(cr, uid, [operation_id], context=context)
3984 self.write(cr, uid, [operation_id], {'qty_done': qty}, context=context)
3986 #no existing operation found for the given domain and picking => create a new one
3987 picking_obj = self.pool.get("stock.picking")
3988 picking = picking_obj.browse(cr, uid, picking_id, context=context)
3990 'picking_id': picking_id,
3992 'location_id': picking.location_id.id,
3993 'location_dest_id': picking.location_dest_id.id,
3997 var_name, dummy, value = key
3999 if var_name == 'product_id':
4000 uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
4001 update_dict = {var_name: value}
4003 update_dict['product_uom_id'] = uom_id
4004 values.update(update_dict)
4005 operation_id = self.create(cr, uid, values, context=context)
4009 class stock_move_operation_link(osv.osv):
4011 Table making the link between stock.moves and stock.pack.operations to compute the remaining quantities on each of these objects
4013 _name = "stock.move.operation.link"
4014 _description = "Link between stock moves and pack operations"
4017 'qty': fields.float('Quantity', help="Quantity of products to consider when talking about the contribution of this pack operation towards the remaining quantity of the move (and inverse). Given in the product main uom."),
4018 'operation_id': fields.many2one('stock.pack.operation', 'Operation', required=True, ondelete="cascade"),
4019 'move_id': fields.many2one('stock.move', 'Move', required=True, ondelete="cascade"),
4020 'reserved_quant_id': fields.many2one('stock.quant', 'Reserved Quant', help="Technical field containing the quant that created this link between an operation and a stock move. Used at the stock_move_obj.action_done() time to avoid seeking a matching quant again"),
4023 def get_specific_domain(self, cr, uid, record, context=None):
4024 '''Returns the specific domain to consider for quant selection in action_assign() or action_done() of stock.move,
4025 having the record given as parameter making the link between the stock move and a pack operation'''
4027 op = record.operation_id
4029 if op.package_id and op.product_id:
4030 #if removing a product from a box, we restrict the choice of quants to this box
4031 domain.append(('package_id', '=', op.package_id.id))
4033 #if moving a box, we allow to take everything from inside boxes as well
4034 domain.append(('package_id', 'child_of', [op.package_id.id]))
4036 #if not given any information about package, we don't open boxes
4037 domain.append(('package_id', '=', False))
4038 #if lot info is given, we restrict choice to this lot otherwise we can take any
4040 domain.append(('lot_id', '=', op.lot_id.id))
4041 #if owner info is given, we restrict to this owner otherwise we restrict to no owner
4043 domain.append(('owner_id', '=', op.owner_id.id))
4045 domain.append(('owner_id', '=', False))
4048 class stock_warehouse_orderpoint(osv.osv):
4050 Defines Minimum stock rules.
4052 _name = "stock.warehouse.orderpoint"
4053 _description = "Minimum Inventory Rule"
4055 def subtract_procurements(self, cr, uid, orderpoint, context=None):
4056 '''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it.
4059 uom_obj = self.pool.get("product.uom")
4060 for procurement in orderpoint.procurement_ids:
4061 if procurement.state in ('cancel', 'done'):
4063 procurement_qty = uom_obj._compute_qty_obj(cr, uid, procurement.product_uom, procurement.product_qty, procurement.product_id.uom_id, context=context)
4064 for move in procurement.move_ids:
4065 #need to add the moves in draft as they aren't in the virtual quantity + moves that have not been created yet
4066 if move.state not in ('draft'):
4067 #if move is already confirmed, assigned or done, the virtual stock is already taking this into account so it shouldn't be deducted
4068 procurement_qty -= move.product_qty
4069 qty += procurement_qty
4072 def _check_product_uom(self, cr, uid, ids, context=None):
4074 Check if the UoM has the same category as the product standard UoM
4079 for rule in self.browse(cr, uid, ids, context=context):
4080 if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
4084 def action_view_proc_to_process(self, cr, uid, ids, context=None):
4085 act_obj = self.pool.get('ir.actions.act_window')
4086 mod_obj = self.pool.get('ir.model.data')
4087 proc_ids = self.pool.get('procurement.order').search(cr, uid, [('orderpoint_id', 'in', ids), ('state', 'not in', ('done', 'cancel'))], context=context)
4088 result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
4092 result = act_obj.read(cr, uid, [result[1]], context=context)[0]
4093 result['domain'] = "[('id', 'in', [" + ','.join(map(str, proc_ids)) + "])]"
4097 'name': fields.char('Name', required=True, copy=False),
4098 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
4099 'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
4100 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
4101 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
4102 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '=', 'product')]),
4103 'product_uom': fields.related('product_id', 'uom_id', type='many2one', relation='product.uom', string='Product Unit of Measure', readonly=True, required=True),
4104 'product_min_qty': fields.float('Minimum Quantity', required=True,
4105 digits_compute=dp.get_precision('Product Unit of Measure'),
4106 help="When the virtual stock goes below the Min Quantity specified for this field, Odoo generates "\
4107 "a procurement to bring the forecasted quantity to the Max Quantity."),
4108 'product_max_qty': fields.float('Maximum Quantity', required=True,
4109 digits_compute=dp.get_precision('Product Unit of Measure'),
4110 help="When the virtual stock goes below the Min Quantity, Odoo generates "\
4111 "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
4112 'qty_multiple': fields.float('Qty Multiple', required=True,
4113 digits_compute=dp.get_precision('Product Unit of Measure'),
4114 help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used. "),
4115 'procurement_ids': fields.one2many('procurement.order', 'orderpoint_id', 'Created Procurements'),
4116 'group_id': fields.many2one('procurement.group', 'Procurement Group', help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking.", copy=False),
4117 'company_id': fields.many2one('res.company', 'Company', required=True),
4120 'active': lambda *a: 1,
4121 'logic': lambda *a: 'max',
4122 'qty_multiple': lambda *a: 1,
4123 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').next_by_code(cr, uid, 'stock.orderpoint') or '',
4124 'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
4125 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
4127 _sql_constraints = [
4128 ('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
4131 (_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']),
4134 def default_get(self, cr, uid, fields, context=None):
4135 warehouse_obj = self.pool.get('stock.warehouse')
4136 res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
4137 # default 'warehouse_id' and 'location_id'
4138 if 'warehouse_id' not in res:
4139 warehouse_ids = res.get('company_id') and warehouse_obj.search(cr, uid, [('company_id', '=', res['company_id'])], limit=1, context=context) or []
4140 res['warehouse_id'] = warehouse_ids and warehouse_ids[0] or False
4141 if 'location_id' not in res:
4142 res['location_id'] = res.get('warehouse_id') and warehouse_obj.browse(cr, uid, res['warehouse_id'], context).lot_stock_id.id or False
4145 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
4146 """ Finds location id for changed warehouse.
4147 @param warehouse_id: Changed id of warehouse.
4148 @return: Dictionary of values.
4151 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
4152 v = {'location_id': w.lot_stock_id.id}
4156 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
4157 """ Finds UoM for changed product.
4158 @param product_id: Changed id of product.
4159 @return: Dictionary of values.
4162 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
4163 d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
4164 v = {'product_uom': prod.uom_id.id}
4165 return {'value': v, 'domain': d}
4166 return {'domain': {'product_uom': []}}
4168 class stock_picking_type(osv.osv):
4169 _name = "stock.picking.type"
4170 _description = "The picking type determines the picking view"
4173 def open_barcode_interface(self, cr, uid, ids, context=None):
4174 final_url = "/stock/barcode/#action=stock.ui&picking_type_id=" + str(ids[0]) if len(ids) else '0'
4175 return {'type': 'ir.actions.act_url', 'url': final_url, 'target': 'self'}
4177 def _get_tristate_values(self, cr, uid, ids, field_name, arg, context=None):
4178 picking_obj = self.pool.get('stock.picking')
4180 for picking_type_id in ids:
4181 #get last 10 pickings of this type
4182 picking_ids = picking_obj.search(cr, uid, [('picking_type_id', '=', picking_type_id), ('state', '=', 'done')], order='date_done desc', limit=10, context=context)
4184 for picking in picking_obj.browse(cr, uid, picking_ids, context=context):
4185 if picking.date_done > picking.date:
4186 tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1})
4187 elif picking.backorder_id:
4188 tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0})
4190 tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1})
4191 res[picking_type_id] = json.dumps(tristates)
4194 def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
4195 obj = self.pool.get('stock.picking')
4197 'count_picking_draft': [('state', '=', 'draft')],
4198 'count_picking_waiting': [('state', '=', 'confirmed')],
4199 'count_picking_ready': [('state', 'in', ('assigned', 'partially_available'))],
4200 'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
4201 'count_picking_late': [('min_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
4202 'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting', 'partially_available'))],
4205 for field in domains:
4206 data = obj.read_group(cr, uid, domains[field] +
4207 [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', ids)],
4208 ['picking_type_id'], ['picking_type_id'], context=context)
4209 count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
4211 result.setdefault(tid, {})[field] = count.get(tid, 0)
4213 if result[tid]['count_picking']:
4214 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] * 100 / result[tid]['count_picking']
4215 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] * 100 / result[tid]['count_picking']
4217 result[tid]['rate_picking_late'] = 0
4218 result[tid]['rate_picking_backorders'] = 0
4221 def onchange_picking_code(self, cr, uid, ids, picking_code=False):
4222 if not picking_code:
4225 obj_data = self.pool.get('ir.model.data')
4226 stock_loc = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_stock')
4229 'default_location_src_id': stock_loc,
4230 'default_location_dest_id': stock_loc,
4232 if picking_code == 'incoming':
4233 result['default_location_src_id'] = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_suppliers')
4234 elif picking_code == 'outgoing':
4235 result['default_location_dest_id'] = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_customers')
4236 return {'value': result}
4238 def _get_name(self, cr, uid, ids, field_names, arg, context=None):
4239 return dict(self.name_get(cr, uid, ids, context=context))
4241 def name_get(self, cr, uid, ids, context=None):
4242 """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
4245 if not isinstance(ids, list):
4250 for record in self.browse(cr, uid, ids, context=context):
4252 if record.warehouse_id:
4253 name = record.warehouse_id.name + ': ' +name
4254 if context.get('special_shortened_wh_name'):
4255 if record.warehouse_id:
4256 name = record.warehouse_id.name
4258 name = _('Customer') + ' (' + record.name + ')'
4259 res.append((record.id, name))
4262 def _default_warehouse(self, cr, uid, context=None):
4263 user = self.pool.get('res.users').browse(cr, uid, uid, context)
4264 res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
4265 return res and res[0] or False
4268 'name': fields.char('Picking Type Name', translate=True, required=True),
4269 'complete_name': fields.function(_get_name, type='char', string='Name'),
4270 'color': fields.integer('Color'),
4271 'sequence': fields.integer('Sequence', help="Used to order the 'All Operations' kanban view"),
4272 'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
4273 'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
4274 'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
4275 'code': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True),
4276 'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
4277 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
4278 'active': fields.boolean('Active'),
4280 # Statistics for the kanban view
4281 'last_done_picking': fields.function(_get_tristate_values,
4283 string='Last 10 Done Pickings'),
4285 'count_picking_draft': fields.function(_get_picking_count,
4286 type='integer', multi='_get_picking_count'),
4287 'count_picking_ready': fields.function(_get_picking_count,
4288 type='integer', multi='_get_picking_count'),
4289 'count_picking': fields.function(_get_picking_count,
4290 type='integer', multi='_get_picking_count'),
4291 'count_picking_waiting': fields.function(_get_picking_count,
4292 type='integer', multi='_get_picking_count'),
4293 'count_picking_late': fields.function(_get_picking_count,
4294 type='integer', multi='_get_picking_count'),
4295 'count_picking_backorders': fields.function(_get_picking_count,
4296 type='integer', multi='_get_picking_count'),
4298 'rate_picking_late': fields.function(_get_picking_count,
4299 type='integer', multi='_get_picking_count'),
4300 'rate_picking_backorders': fields.function(_get_picking_count,
4301 type='integer', multi='_get_picking_count'),
4305 'warehouse_id': _default_warehouse,
4309 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: