1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from datetime import date, datetime
23 from dateutil import relativedelta
27 from openerp.osv import fields, osv
28 from openerp.tools.translate import _
29 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
30 from openerp import SUPERUSER_ID
31 import openerp.addons.decimal_precision as dp
33 _logger = logging.getLogger(__name__)
36 #----------------------------------------------------------
38 #----------------------------------------------------------
39 class stock_incoterms(osv.osv):
40 _name = "stock.incoterms"
41 _description = "Incoterms"
43 'name': fields.char('Name', size=64, required=True, help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices."),
44 'code': fields.char('Code', size=3, required=True, help="Incoterm Standard Code"),
45 'active': fields.boolean('Active', help="By unchecking the active field, you may hide an INCOTERM you will not use."),
51 #----------------------------------------------------------
53 #----------------------------------------------------------
55 class stock_location(osv.osv):
56 _name = "stock.location"
57 _description = "Inventory Locations"
58 _parent_name = "location_id"
60 _parent_order = 'name'
61 _order = 'parent_left'
62 _rec_name = 'complete_name'
64 def _complete_name(self, cr, uid, ids, name, args, context=None):
65 """ Forms complete name of location from parent location to child location.
66 @return: Dictionary of values
69 for m in self.browse(cr, uid, ids, context=context):
71 parent = m.location_id
73 res[m.id] = parent.name + ' / ' + res[m.id]
74 parent = parent.location_id
77 def _get_sublocations(self, cr, uid, ids, context=None):
78 """ return all sublocations of the given stock locations (included) """
81 context_with_inactive = context.copy()
82 context_with_inactive['active_test'] = False
83 return self.search(cr, uid, [('id', 'child_of', ids)], context=context_with_inactive)
85 def _name_get(self, cr, uid, location, context=None):
87 while location.location_id and location.usage != 'view':
88 location = location.location_id
89 name = location.name + '/' + name
92 def name_get(self, cr, uid, ids, context=None):
94 for location in self.browse(cr, uid, ids, context=context):
95 res.append((location.id, self._name_get(cr, uid, location, context=context)))
99 'name': fields.char('Location Name', size=64, required=True, translate=True),
100 'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
101 'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Type', required=True,
102 help="""* Supplier Location: Virtual location representing the source location for products coming from your suppliers
103 \n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products
104 \n* Internal Location: Physical locations inside your own warehouses,
105 \n* Customer Location: Virtual location representing the destination location for products sent to your customers
106 \n* Inventory: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)
107 \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.
108 \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
111 'complete_name': fields.function(_complete_name, type='char', string="Location Name",
112 store={'stock.location': (_get_sublocations, ['name', 'location_id', 'active'], 10)}),
113 'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
114 'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
116 'partner_id': fields.many2one('res.partner', 'Owner', help="Owner of the location if not internal"),
118 'comment': fields.text('Additional Information'),
119 'posx': fields.integer('Corridor (X)', help="Optional localization details, for information purpose only"),
120 'posy': fields.integer('Shelves (Y)', help="Optional localization details, for information purpose only"),
121 'posz': fields.integer('Height (Z)', help="Optional localization details, for information purpose only"),
123 'parent_left': fields.integer('Left Parent', select=1),
124 'parent_right': fields.integer('Right Parent', select=1),
126 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between all companies'),
127 'scrap_location': fields.boolean('Is a Scrap Location?', help='Check this box to allow using this location to put scrapped/damaged goods.'),
128 'removal_strategy_ids': fields.one2many('product.removal', 'location_id', 'Removal Strategies'),
129 'putaway_strategy_ids': fields.one2many('product.putaway', 'location_id', 'Put Away Strategies'),
134 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
138 'scrap_location': False,
141 def get_putaway_strategy(self, cr, uid, location, product, context=None):
142 pa = self.pool.get('product.putaway')
143 categ = product.categ_id
144 categs = [categ.id, False]
145 while categ.parent_id:
146 categ = categ.parent_id
147 categs.append(categ.id)
149 result = pa.search(cr, uid, [('location_id', '=', location.id), ('product_categ_id', 'in', categs)], context=context)
151 return pa.browse(cr, uid, result[0], context=context)
153 def get_removal_strategy(self, cr, uid, location, product, context=None):
154 pr = self.pool.get('product.removal')
155 categ = product.categ_id
156 categs = [categ.id, False]
157 while categ.parent_id:
158 categ = categ.parent_id
159 categs.append(categ.id)
161 result = pr.search(cr, uid, [('location_id', '=', location.id), ('product_categ_id', 'in', categs)], context=context)
163 return pr.browse(cr, uid, result[0], context=context).method
166 #----------------------------------------------------------
168 #----------------------------------------------------------
170 class stock_location_route(osv.osv):
171 _name = 'stock.location.route'
172 _description = "Inventory Routes"
176 'name': fields.char('Route Name', required=True),
177 'sequence': fields.integer('Sequence'),
178 'pull_ids': fields.one2many('procurement.rule', 'route_id', 'Pull Rules'),
179 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it."),
180 'push_ids': fields.one2many('stock.location.path', 'route_id', 'Push Rules'),
181 'product_selectable': fields.boolean('Applicable on Product'),
182 'product_categ_selectable': fields.boolean('Applicable on Product Category'),
183 'warehouse_selectable': fields.boolean('Applicable on Warehouse'),
184 'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'),
185 'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'),
186 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this route is shared between all companies'),
190 'sequence': lambda self, cr, uid, ctx: 0,
192 'product_selectable': True,
193 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location.route', context=c),
197 #----------------------------------------------------------
199 #----------------------------------------------------------
201 class stock_quant(osv.osv):
203 Quants are the smallest unit of stock physical instances
205 _name = "stock.quant"
206 _description = "Quants"
208 def _get_quant_name(self, cr, uid, ids, name, args, context=None):
209 """ Forms complete name of location from parent location to child location.
210 @return: Dictionary of values
213 for q in self.browse(cr, uid, ids, context=context):
215 res[q.id] = q.product_id.code or ''
217 res[q.id] = q.lot_id.name
218 res[q.id] += ': ' + str(q.qty) + q.product_id.uom_id.name
221 def _calc_inventory_value(self, cr, uid, ids, name, attr, context=None):
223 uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
224 for quant in self.browse(cr, uid, ids, context=context):
225 context.pop('force_company', None)
226 if quant.company_id.id != uid_company_id:
227 #if the company of the quant is different than the current user company, force the company in the context
228 #then re-do a browse to read the property fields for the good company.
229 context['force_company'] = quant.company_id.id
230 quant = self.browse(cr, uid, quant.id, context=context)
231 res[quant.id] = self._get_inventory_value(cr, uid, quant, context=context)
234 def _get_inventory_value(self, cr, uid, quant, context=None):
235 return quant.product_id.standard_price * quant.qty
238 'name': fields.function(_get_quant_name, type='char', string='Identifier'),
239 'product_id': fields.many2one('product.product', 'Product', required=True),
240 'location_id': fields.many2one('stock.location', 'Location', required=True),
241 'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product"),
242 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant"),
243 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', store=True),
244 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for"),
245 'link_move_operation_id': fields.many2one('stock.move.operation.link', 'Reserved for Link between Move and Pack Operation', help="Technical field decpicting for with tuple (move, operation) this quant is reserved for"),
246 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
247 'cost': fields.float('Unit Cost'),
248 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant"),
250 'create_date': fields.datetime('Creation Date'),
251 'in_date': fields.datetime('Incoming Date'),
253 'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
254 'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True),
256 # Used for negative quants to reconcile after compensated by a new positive one
257 'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from'),
258 '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.'),
259 'negative_dest_location_id': fields.related('negative_move_id', 'location_dest_id', type='many2one', relation='stock.location', string="Negative Destination Location",
260 help="Technical field used to record the destination location of a move that created a negative quant"),
261 'inventory_value': fields.function(_calc_inventory_value, string="Inventory Value", type='float', readonly=True),
265 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.quant', context=c),
268 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
269 ''' Overwrite the read_group in order to sum the function field 'inventory_value' in group by'''
270 res = super(stock_quant, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
271 if 'inventory_value' in fields:
273 if '__domain' in line:
274 lines = self.search(cr, uid, line['__domain'], context=context)
276 for line2 in self.browse(cr, uid, lines, context=context):
277 inv_value += line2.inventory_value
278 line['inventory_value'] = inv_value
281 def action_view_quant_history(self, cr, uid, ids, context=None):
283 This function returns an action that display the history of the quant, which
284 mean all the stock moves that lead to this quant creation with this quant quantity.
286 mod_obj = self.pool.get('ir.model.data')
287 act_obj = self.pool.get('ir.actions.act_window')
289 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_move_form2')
290 id = result and result[1] or False
291 result = act_obj.read(cr, uid, [id], context={})[0]
294 for quant in self.browse(cr, uid, ids, context=context):
295 move_ids += [move.id for move in quant.history_ids]
297 result['domain'] = "[('id','in',[" + ','.join(map(str, move_ids)) + "])]"
300 def quants_reserve(self, cr, uid, quants, move, link=False, context=None):
301 '''This function reserves quants for the given move (and optionally given link). If the total of quantity reserved is enough, the move's state
302 is also set to 'assigned'
304 :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
305 :param move: browse record
306 :param link: browse record (stock.move.operation.link)
309 #split quants if needed
310 for quant, qty in quants:
311 if qty <= 0.0 or (quant and quant.qty <= 0.0):
312 raise osv.except_osv(_('Error!'), _('You can not reserve a negative quantity or a negative quant.'))
315 self._quant_split(cr, uid, quant, qty, context=context)
316 toreserve.append(quant.id)
319 self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id, 'link_move_operation_id': link and link.id or False}, context=context)
320 #check if move'state needs to be set as 'assigned'
322 if move.reserved_availability == move.product_qty and move.state in ('confirmed', 'waiting'):
323 self.pool.get('stock.move').write(cr, uid, [move.id], {'state': 'assigned'}, context=context)
325 def quants_move(self, cr, uid, quants, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, context=None):
326 """Moves all given stock.quant in the destination location of the given move.
328 :param quants: list of tuple(browse record(stock.quant) or None, quantity to move)
329 :param move: browse record (stock.move)
330 :param lot_id: ID of the lot that mus be set on the quants to move
331 :param owner_id: ID of the partner that must own the quants to move
332 :param src_package_id: ID of the package that contains the quants to move
333 :param dest_package_id: ID of the package that must be set on the moved quant
335 for quant, qty in quants:
337 #If quant is None, we will create a quant to move (and potentially a negative counterpart too)
338 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, context=context)
339 self.move_single_quant_tuple(cr, uid, quant, qty, move, context=context)
341 def check_preferred_location(self, cr, uid, move, qty, context=None):
342 '''Checks the preferred location on the move, if any returned by a putaway strategy, and returns a list of
343 tuple(location, qty) where the quant have to be moved
345 :param move: browse record (stock.move)
347 :returns: list of tuple build as [(browe record (stock.move), float)]
351 for record in move.putaway_ids:
352 res.append((record.location_id, record.quantity))
354 return [(move.location_dest_id, qty)]
356 def move_single_quant(self, cr, uid, quant, location_to, qty, move, context=None):
357 '''Moves the given 'quant' in 'location_to' for the given 'qty', and logs the stock.move that triggered this move in the quant history.
358 If needed, the quant may be split if it's not totally moved.
360 :param quant: browse record (stock.quant)
361 :param location_to: browse record (stock.location)
363 :param move: browse record (stock.move)
365 new_quant = self._quant_split(cr, uid, quant, qty, context=context)
367 'location_id': location_to.id,
368 'history_ids': [(4, move.id)],
370 #if the quant we are moving had been split and was inside a package, it means we unpacked it
371 if new_quant and new_quant.package_id:
372 vals['package_id'] = False
373 self.write(cr, SUPERUSER_ID, [quant.id], vals, context=context)
377 def move_single_quant_tuple(self, cr, uid, quant, qty, move, context=None):
378 '''Effectively process the move of a tuple (quant record, qty to move). This may result in several quants moved
379 if the preferred locations on the move say so but by default it will only move the quant record given as argument
380 :param quant: browse record (stock.quant)
382 :param move: browse record (stock.move)
384 for location_to, qty in self.check_preferred_location(cr, uid, move, qty, context=context):
387 new_quant = self.move_single_quant(cr, uid, quant, location_to, qty, move, context=context)
388 self._quant_reconcile_negative(cr, uid, quant, move, context=context)
391 def quants_get_prefered_domain(self, cr, uid, location, product, qty, domain=None, prefered_domain=False, fallback_domain=False, restrict_lot_id=False, restrict_partner_id=False, context=None):
392 ''' This function tries to find quants in the given location for the given domain, by trying to first limit
393 the choice on the quants that match the prefered_domain as well. But if the qty requested is not reached
394 it tries to find the remaining quantity by using the fallback_domain.
396 if prefered_domain and fallback_domain:
399 quants = self.quants_get(cr, uid, location, product, qty, domain=domain + prefered_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
404 quant_ids.append(quant[0].id)
407 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
409 #make sure the quants aren't found twice (if the prefered_domain and the fallback_domain aren't orthogonal
410 domain += [('id', 'not in', quant_ids)]
411 unprefered_quants = self.quants_get(cr, uid, location, product, res_qty, domain=domain + fallback_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
412 for quant in unprefered_quants:
415 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)
417 def quants_get(self, cr, uid, location, product, qty, domain=None, restrict_lot_id=False, restrict_partner_id=False, context=None):
419 Use the removal strategies of product to search for the correct quants
420 If you inherit, put the super at the end of your method.
422 :location: browse record of the parent location where the quants have to be found
423 :product: browse record of the product to find
424 :qty in UoM of product
427 domain = domain or [('qty', '>', 0.0)]
428 if restrict_partner_id:
429 domain += [('owner_id', '=', restrict_partner_id)]
431 domain += [('lot_id', '=', restrict_lot_id)]
433 removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) or 'fifo'
434 if removal_strategy == 'fifo':
435 result += self._quants_get_fifo(cr, uid, location, product, qty, domain, context=context)
436 elif removal_strategy == 'lifo':
437 result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context)
439 raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,)))
442 def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location=False, context=None):
443 '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location.
447 price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
448 location = force_location or move.location_dest_id
450 'product_id': move.product_id.id,
451 'location_id': location.id,
454 'history_ids': [(4, move.id)],
455 'in_date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
456 'company_id': move.company_id.id,
458 'owner_id': owner_id,
459 'package_id': dest_package_id,
462 if move.location_id.usage == 'internal':
463 #if we were trying to move something from an internal location and reach here (quant creation),
464 #it means that a negative quant has to be created as well.
465 negative_vals = vals.copy()
466 negative_vals['location_id'] = move.location_id.id
467 negative_vals['qty'] = -qty
468 negative_vals['cost'] = price_unit
469 negative_vals['negative_move_id'] = move.id
470 negative_vals['package_id'] = src_package_id
471 negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
472 vals.update({'propagated_from_id': negative_quant_id})
474 #create the quant as superuser, because we want to restrict the creation of quant manually: they should always use this method to create quants
475 quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
476 return self.browse(cr, uid, quant_id, context=context)
478 def _quant_split(self, cr, uid, quant, qty, context=None):
479 context = context or {}
480 if (quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty):
482 new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty}, context=context)
483 self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context)
485 return self.browse(cr, uid, new_quant, context=context)
487 def _get_latest_move(self, cr, uid, quant, context=None):
489 for m in quant.history_ids:
490 if not move or m.date > move.date:
494 def _quants_merge(self, cr, uid, solved_quant_ids, solving_quant, context=None):
496 for move in solving_quant.history_ids:
497 path.append((4, move.id))
498 self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
500 def _quant_reconcile_negative(self, cr, uid, quant, move, context=None):
502 When new quant arrive in a location, try to reconcile it with
503 negative quants. If it's possible, apply the cost of the new
504 quant to the conter-part of the negative quant.
506 if quant.location_id.usage != 'internal':
508 solving_quant = quant
509 dom = [('qty', '<', 0)]
511 dom += [('lot_id', '=', quant.lot_id.id)]
512 dom += [('owner_id', '=', quant.owner_id.id)]
513 dom += [('package_id', '=', quant.package_id.id)]
514 if move.move_dest_id:
515 dom += [('negative_move_id', '=', move.move_dest_id.id)]
516 quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, dom, context=context)
517 for quant_neg, qty in quants:
520 to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
521 if not to_solve_quant_ids:
524 solved_quant_ids = []
525 for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
528 solved_quant_ids.append(to_solve_quant.id)
529 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
530 solving_qty -= min(solving_qty, to_solve_quant.qty)
531 remaining_solving_quant = self._quant_split(cr, uid, solving_quant, qty, context=context)
532 remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
533 #if the reconciliation was not complete, we need to link together the remaining parts
534 if remaining_neg_quant:
535 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
536 if remaining_to_solve_quant_ids:
537 self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
538 #delete the reconciled quants, as it is replaced by the solved quants
539 self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
540 #price update + accounting entries adjustments
541 self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
542 #merge history (and cost?)
543 self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
544 self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
545 solving_quant = remaining_solving_quant
547 def _price_update(self, cr, uid, ids, newprice, context=None):
548 self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context)
550 def quants_unreserve(self, cr, uid, move, context=None):
551 related_quants = [x.id for x in move.reserved_quant_ids]
552 return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'link_move_operation_id': False}, context=context)
554 def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
555 ''' Implementation of removal strategies
556 If it can not reserve, it will return a tuple (None, qty)
558 domain += location and [('location_id', 'child_of', location.id)] or []
559 domain += [('product_id', '=', product.id)] + domain
560 #don't take into account location that are production, supplier or inventory
561 ignore_location_ids = self.pool.get('stock.location').search(cr, uid, [('usage', 'in', ('production', 'supplier', 'inventory'))], context=context)
562 domain.append(('location_id','not in',ignore_location_ids))
566 quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
568 res.append((None, quantity))
570 for quant in self.browse(cr, uid, quants, context=context):
571 if quantity >= abs(quant.qty):
572 res += [(quant, abs(quant.qty))]
573 quantity -= abs(quant.qty)
575 res += [(quant, quantity)]
581 def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None):
583 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
585 def _quants_get_lifo(self, cr, uid, location, product, quantity, domain=[], context=None):
586 order = 'in_date desc'
587 return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
589 def _location_owner(self, cr, uid, quant, location, context=None):
590 ''' Return the company owning the location if any '''
591 return location and (location.usage == 'internal') and location.company_id or False
593 def _check_location(self, cr, uid, ids, context=None):
594 for record in self.browse(cr, uid, ids, context=context):
595 if record.location_id.usage == 'view':
596 raise osv.except_osv(_('Error'), _('You cannot move product %s to a location of type view %s.') % (record.product_id.name, record.location_id.name))
600 (_check_location, 'You cannot move products to a location of the type view.', ['location_id'])
604 #----------------------------------------------------------
606 #----------------------------------------------------------
608 class stock_picking(osv.osv):
609 _name = "stock.picking"
610 _inherit = ['mail.thread']
611 _description = "Picking List"
612 _order = "priority desc, date asc, id desc"
614 def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
615 move_obj = self.pool.get("stock.move")
617 move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
618 move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
620 def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
621 """ Finds minimum and maximum dates for picking.
622 @return: Dictionary of values
626 res[id] = {'min_date': False, 'max_date': False}
638 picking_id""", (tuple(ids),))
639 for pick, dt1, dt2 in cr.fetchall():
640 res[pick]['min_date'] = dt1
641 res[pick]['max_date'] = dt2
644 def create(self, cr, user, vals, context=None):
645 context = context or {}
646 if ('name' not in vals) or (vals.get('name') in ('/', False)):
647 ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
648 sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
649 vals['name'] = self.pool.get('ir.sequence').get_id(cr, user, sequence_id, 'id', context=context)
650 return super(stock_picking, self).create(cr, user, vals, context)
652 def _state_get(self, cr, uid, ids, field_name, arg, context=None):
653 '''The state of a picking depends on the state of its related stock.move
654 draft: the picking has no line or any one of the lines is draft
655 done, draft, cancel: all lines are done / draft / cancel
656 confirmed, auto, assigned depends on move_type (all at once or direct)
659 for pick in self.browse(cr, uid, ids, context=context):
660 if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]):
661 res[pick.id] = 'draft'
663 if all([x.state == 'cancel' for x in pick.move_lines]):
664 res[pick.id] = 'cancel'
666 if all([x.state in ('cancel', 'done') for x in pick.move_lines]):
667 res[pick.id] = 'done'
670 order = {'confirmed': 0, 'waiting': 1, 'assigned': 2}
671 order_inv = {0: 'confirmed', 1: 'waiting', 2: 'assigned'}
672 lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel', 'done')]
673 if pick.move_type == 'one':
674 res[pick.id] = order_inv[min(lst)]
676 #we are in the case of partial delivery, so if all move are assigned, picking
677 #should be assign too, else if one of the move is assigned, or partially available, picking should be
678 #in partially available state, otherwise, picking is in waiting or confirmed state
679 res[pick.id] = order_inv[max(lst)]
680 if not all(x == 2 for x in lst):
681 #if all moves aren't assigned, check if we have one product partially available
682 for move in pick.move_lines:
683 if move.reserved_quant_ids:
684 res[pick.id] = 'partially_available'
688 def _get_pickings(self, cr, uid, ids, context=None):
690 for move in self.browse(cr, uid, ids, context=context):
692 res.add(move.picking_id.id)
695 def _get_pickings_from_quant(self, cr, uid, ids, context=None):
697 for quant in self.browse(cr, uid, ids, context=context):
698 if quant.reservation_id and quant.reservation_id.picking_id:
699 res.add(quant.reservation_id.picking_id.id)
702 def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
704 for pick in self.browse(cr, uid, ids, context=context):
706 if pick.pack_operation_ids:
710 def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
712 for pick in self.browse(cr, uid, ids, context=context):
714 for move in pick.move_lines:
715 if move.reserved_quant_ids:
720 def action_assign_owner(self, cr, uid, ids, context=None):
721 for picking in self.browse(cr, uid, ids, context=context):
722 packop_ids = [op.id for op in picking.pack_operation_ids]
723 self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
726 'name': fields.char('Reference', size=64, select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
727 'origin': fields.char('Source Document', size=64, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document", select=True),
728 '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),
729 'note': fields.text('Notes', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
730 '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"),
731 'state': fields.function(_state_get, type="selection", store={
732 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'move_lines'], 20),
733 'stock.move': (_get_pickings, ['state', 'picking_id'], 20),
734 'stock.quant': (_get_pickings_from_quant, ['reservation_id'], 20)}, selection=[
736 ('cancel', 'Cancelled'),
737 ('waiting', 'Waiting Another Operation'),
738 ('confirmed', 'Waiting Availability'),
739 ('partially_available', 'Partially Available'),
740 ('assigned', 'Ready to Transfer'),
741 ('done', 'Transferred'),
742 ], string='Status', readonly=True, select=True, track_visibility='onchange', help="""
743 * Draft: not confirmed yet and will not be scheduled until confirmed\n
744 * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
745 * Waiting Availability: still waiting for the availability of products\n
746 * Partially Available: some products are available and reserved\n
747 * Ready to Transfer: products reserved, simply waiting for confirmation.\n
748 * Transferred: has been processed, can't be modified or cancelled anymore\n
749 * Cancelled: has been cancelled, can't be confirmed anymore"""
751 'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Priority', required=True),
752 'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
753 store={'stock.move': (_get_pickings, ['state', 'date_expected'], 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'),
754 'max_date': fields.function(get_min_max_date, multi="min_max_date",
755 store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
756 'date': fields.datetime('Commitment Date', help="Date promised for the completion of the transfer order, usually set the time of the order and revised later on.", select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, track_visibility='onchange'),
757 'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
758 'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
759 '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'),
760 'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
761 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
762 'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Related Packing Operations'),
763 'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
764 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, required=True),
765 '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"),
767 'owner_id': fields.many2one('res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner"),
768 # Used to search on pickings
769 'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),
770 'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
771 'location_dest_id': fields.related('move_lines', 'location_dest_id', type='many2one', relation='stock.location', string='Destination Location', readonly=True),
772 'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True,
774 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
775 'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
780 'name': lambda self, cr, uid, context: '/',
783 'priority': '1', # normal
784 'date': fields.datetime.now,
785 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c)
788 ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
791 def copy(self, cr, uid, id, default=None, context=None):
794 default = default.copy()
795 picking_obj = self.browse(cr, uid, id, context=context)
796 if ('name' not in default) or (picking_obj.name == '/'):
797 default['name'] = '/'
798 if not default.get('backorder_id'):
799 default['backorder_id'] = False
800 default['pack_operation_ids'] = []
801 default['date_done'] = False
802 return super(stock_picking, self).copy(cr, uid, id, default, context)
804 def do_print_delivery(self, cr, uid, ids, context=None):
805 '''This function prints the delivery order'''
806 assert len(ids) == 1, 'This option should only be used for a single id at a time'
808 'model': 'stock.picking',
811 return {'type': 'ir.actions.report.xml', 'report_name': 'stock.picking.list', 'datas': datas, 'nodestroy': True}
813 def do_print_picking(self, cr, uid, ids, context=None):
814 '''This function prints the picking list'''
815 assert len(ids) == 1, 'This option should only be used for a single id at a time'
817 'model': 'stock.picking',
820 return {'type': 'ir.actions.report.xml', 'report_name': 'stock.picking.list.internal', 'datas': datas, 'nodestroy': True}
822 def action_confirm(self, cr, uid, ids, context=None):
824 todo_force_assign = []
825 for picking in self.browse(cr, uid, ids, context=context):
826 if picking.picking_type_id.auto_force_assign:
827 todo_force_assign.append(picking.id)
828 for r in picking.move_lines:
829 if r.state == 'draft':
832 self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
834 if todo_force_assign:
835 self.force_assign(cr, uid, todo_force_assign, context=context)
838 def action_assign(self, cr, uid, ids, context=None):
839 """ Check availability of picking moves.
840 This has the effect of changing the state and reserve quants on available moves, and may
841 also impact the state of the picking as it is computed based on move's states.
844 for pick in self.browse(cr, uid, ids, context=context):
845 if pick.state == 'draft':
846 self.action_confirm(cr, uid, [pick.id], context=context)
848 #skip the moves that don't need to be checked
849 move_ids = [x.id for x in pick.move_lines if x.state not in ('draft', 'cancel', 'done')]
851 raise osv.except_osv(_('Warning!'), _('Nothing to check the availability for.'))
852 self.pool.get('stock.move').action_assign(cr, uid, move_ids, context=context)
855 def force_assign(self, cr, uid, ids, context=None):
856 """ Changes state of picking to available if moves are confirmed or waiting.
859 for pick in self.browse(cr, uid, ids, context=context):
860 move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
861 self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
862 if pick.pack_operation_exist:
863 self.do_prepare_partial(cr, uid, [pick.id], context=None)
866 def action_cancel(self, cr, uid, ids, context=None):
867 for pick in self.browse(cr, uid, ids, context=context):
868 ids2 = [move.id for move in pick.move_lines]
869 self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
872 def action_done(self, cr, uid, ids, context=None):
873 """Changes picking state to done by processing the Stock Moves of the Picking
875 Normally that happens when the button "Done" is pressed on a Picking view.
878 for pick in self.browse(cr, uid, ids, context=context):
880 for move in pick.move_lines:
881 if move.state == 'draft':
882 todo.extend(self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context))
883 elif move.state in ('assigned', 'confirmed'):
886 self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
889 def unlink(self, cr, uid, ids, context=None):
890 #on picking deletion, cancel its move then unlink them too
891 move_obj = self.pool.get('stock.move')
892 context = context or {}
893 for pick in self.browse(cr, uid, ids, context=context):
894 move_ids = [move.id for move in pick.move_lines]
895 move_obj.action_cancel(cr, uid, move_ids, context=context)
896 move_obj.unlink(cr, uid, move_ids, context=context)
897 return super(stock_picking, self).unlink(cr, uid, ids, context=context)
899 def write(self, cr, uid, ids, vals, context=None):
900 res = super(stock_picking, self).write(cr, uid, ids, vals, context=context)
901 #if we changed the move lines or the pack operations, we need to recompute the remaining quantities of both
902 if 'move_lines' in vals or 'pack_operation_ids' in vals:
903 self.do_recompute_remaining_quantities(cr, uid, ids, context=context)
906 def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
907 """ 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.
909 if not backorder_moves:
910 backorder_moves = picking.move_lines
911 backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done', 'cancel')]
912 if 'do_only_split' in context and context['do_only_split']:
913 backorder_move_ids = [x.id for x in backorder_moves if x.id not in context.get('split', [])]
915 if backorder_move_ids:
916 backorder_id = self.copy(cr, uid, picking.id, {
919 'pack_operation_ids': [],
920 'backorder_id': picking.id,
922 back_order_name = self.browse(cr, uid, backorder_id, context=context).name
923 self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (back_order_name), context=context)
924 move_obj = self.pool.get("stock.move")
925 move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
927 self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
928 self.action_confirm(cr, uid, [backorder_id], context=context)
932 def recheck_availability(self, cr, uid, picking_ids, context=None):
933 self.action_assign(cr, uid, picking_ids, context=context)
934 self.do_prepare_partial(cr, uid, picking_ids, context=context)
936 def do_prepare_partial(self, cr, uid, picking_ids, context=None):
937 context = context or {}
938 pack_operation_obj = self.pool.get('stock.pack.operation')
939 pack_obj = self.pool.get("stock.quant.package")
940 quant_obj = self.pool.get("stock.quant")
941 #get list of existing operations and delete them
942 existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context)
943 if existing_package_ids:
944 pack_operation_obj.unlink(cr, uid, existing_package_ids, context)
946 for picking in self.browse(cr, uid, picking_ids, context=context):
951 #Calculate packages, reserved quants, qtys of this picking's moves
952 for move in picking.move_lines:
953 if move.state not in ('assigned', 'confirmed'):
955 packages += [x.package_id for x in move.reserved_quant_ids if x.package_id]
956 reserved_move[move.id] = set([x.id for x in move.reserved_quant_ids])
957 if move.state == 'assigned':
958 qty = move.product_qty
960 qty = move.reserved_availability
962 #Add qty to qtys remaining
963 if qtys_remaining.get(move.product_id.id):
964 qtys_remaining[move.product_id.id] += qty
966 qtys_remaining[move.product_id.id] = qty
968 # Try to find as much as possible top-level packages that can be moved
969 top_lvl_packages = set()
970 for pack in packages:
975 quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
976 move_list = [m.id for m in picking.move_lines]
977 if all([(x.reservation_id and x.reservation_id.id in move_list or False) for x in quant_obj.browse(cr, uid, quants, context=context)]):
978 good_pack = test_pack.id
979 if test_pack.parent_id:
980 test_pack = test_pack.parent_id
982 #stop the loop when there's no parent package anymore
985 #stop the loop when the package test_pack is not totally reserved for moves of this picking
986 #(some quants may be reserved for other picking or not reserved at all)
989 top_lvl_packages.add(good_pack)
991 # Create pack operations for the top-level packages found
992 for pack in pack_obj.browse(cr, uid, list(top_lvl_packages), context=context):
993 quants = pack_obj.get_content(cr, uid, [pack.id], context=context)
994 for quant in quant_obj.browse(cr, uid, quants, context=context):
995 reserved_move[quant.reservation_id.id] -= set([quant.id])
996 qtys_remaining[quant.product_id.id] -= quant.qty
997 pack_operation_obj.create(cr, uid, {
998 'picking_id': picking.id,
999 'package_id': pack.id,
1003 # Go through all remaining reserved quants and group by product, package, lot, owner
1005 for move, quant_set in reserved_move.items():
1006 for quant in quant_obj.browse(cr, uid, list(quant_set), context=context):
1007 qtys_remaining[quant.product_id.id] -= quant.qty
1008 key = (quant.product_id.id, quant.package_id.id, quant.lot_id.id, quant.owner_id.id)
1009 if qtys_grouped.get(key):
1010 qtys_grouped[key] += quant.qty
1012 qtys_grouped[key] = quant.qty
1014 # Add remaining qtys (in cases of force_assign for example)
1015 for product in qtys_remaining.keys():
1016 if qtys_remaining[product] > 0:
1017 key = (product, False, False, False)
1018 if qtys_grouped.get(key):
1019 qtys_grouped[key] += qtys_remaining[product]
1021 qtys_grouped[key] = qtys_remaining[product]
1023 # Create the necessary operations for the grouped quants and remaining qtys
1024 for key, qty in qtys_grouped.items():
1025 pack_operation_obj.create(cr, uid, {
1026 'picking_id': picking.id,
1028 'product_id': key[0],
1029 'package_id': key[1],
1032 'product_uom_id': self.pool.get("product.product").browse(cr, uid, key[0], context=context).uom_id.id,
1035 def do_unreserve(self, cr, uid, picking_ids, context=None):
1037 Will remove all quants for picking in picking_ids
1039 moves_to_unreserve = []
1040 pack_line_to_unreserve = []
1041 for picking in self.browse(cr, uid, picking_ids, context=context):
1042 moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')]
1043 pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids]
1044 if moves_to_unreserve:
1045 if pack_line_to_unreserve:
1046 self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
1047 self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context)
1049 def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None):
1050 pack_op_obj = self.pool.get('stock.pack.operation')
1051 for picking in self.browse(cr, uid, picking_ids, context=context):
1052 op_ids = [op.id for op in picking.pack_operation_ids]
1054 pack_op_obj.recompute_rem_qty_from_operation(cr, uid, op_ids, context=context)
1056 def _create_extra_moves(self, cr, uid, picking, context=None):
1057 '''This function creates move lines on a picking, at the time of do_transfer, based on
1058 unexpected product transfers (or exceeding quantities) found in the pack operations.
1060 move_obj = self.pool.get('stock.move')
1061 operation_obj = self.pool.get('stock.pack.operation')
1062 for op in picking.pack_operation_ids:
1063 for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items():
1064 if remaining_qty > 0:
1065 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
1067 'picking_id': picking.id,
1068 'location_id': picking.location_id.id,
1069 'location_dest_id': picking.location_dest_id.id,
1070 'product_id': product_id,
1071 'product_uom': product.uom_id.id,
1072 'product_uom_qty': remaining_qty,
1073 'name': _('Extra Move: ') + product.name,
1074 'state': 'confirmed',
1076 move_obj.create(cr, uid, vals, context=context)
1077 self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
1079 def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
1080 """ Unreserve quants then try to reassign quants."""
1081 stock_move_obj = self.pool.get('stock.move')
1083 self.do_unreserve(cr, uid, [picking.id], context=context)
1084 self.action_assign(cr, uid, [picking.id], context=context)
1086 stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
1087 stock_move_obj.action_assign(cr, uid, move_ids, context=context)
1089 def do_transfer(self, cr, uid, picking_ids, context=None):
1091 If no pack operation, we do simple action_done of the picking
1092 Otherwise, do the pack operations
1096 stock_move_obj = self.pool.get('stock.move')
1097 for picking in self.browse(cr, uid, picking_ids, context=context):
1098 if not picking.pack_operation_ids:
1099 self.action_done(cr, uid, [picking.id], context=context)
1102 self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
1103 #create extra moves in the picking (unexpected product moves coming from pack operations)
1104 self._create_extra_moves(cr, uid, picking, context=context)
1106 #split move lines eventually
1108 toassign_move_ids = []
1109 for move in picking.move_lines:
1110 if move.state in ('done', 'cancel'):
1111 #ignore stock moves cancelled or already done
1113 elif move.state == 'draft':
1114 toassign_move_ids.append(move.id)
1115 if move.remaining_qty == 0:
1116 if move.state in ('draft', 'assigned', 'confirmed'):
1117 todo_move_ids.append(move.id)
1118 elif move.remaining_qty > 0 and move.remaining_qty < move.product_qty:
1119 new_move = stock_move_obj.split(cr, uid, move, move.remaining_qty, context=context)
1120 todo_move_ids.append(move.id)
1121 #Assign move as it was assigned before
1122 toassign_move_ids.append(new_move)
1123 self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context)
1124 if todo_move_ids and not context.get('do_only_split'):
1125 self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context)
1126 elif context.get('do_only_split'):
1127 context.update({'split': todo_move_ids})
1129 self._create_backorder(cr, uid, picking, context=context)
1130 if toassign_move_ids:
1131 stock_move_obj.action_assign(cr, uid, toassign_move_ids, context=context)
1134 def do_split(self, cr, uid, picking_ids, context=None):
1135 """ just split the picking (create a backorder) without making it 'done' """
1138 ctx = context.copy()
1139 ctx['do_only_split'] = True
1140 return self.do_transfer(cr, uid, picking_ids, context=ctx)
1142 def get_next_picking_for_ui(self, cr, uid, context=None):
1143 """ returns the next pickings to process. Used in the barcode scanner UI"""
1146 domain = [('state', 'in', ('assigned', 'partially_available'))]
1147 if context.get('default_picking_type_id'):
1148 domain.append(('picking_type_id', '=', context['default_picking_type_id']))
1149 return self.search(cr, uid, domain, context=context)
1151 def action_done_from_ui(self, cr, uid, picking_id, context=None):
1152 """ called when button 'done' is pushed in the barcode scanner UI """
1153 self.do_transfer(cr, uid, [picking_id], context=context)
1154 #return id of next picking to work on
1155 return self.get_next_picking_for_ui(cr, uid, context=context)
1157 def action_pack(self, cr, uid, picking_ids, context=None):
1158 """ Create a package with the current pack_operation_ids of the picking that aren't yet in a pack.
1159 Used in the barcode scanner UI and the normal interface as well. """
1160 stock_operation_obj = self.pool.get('stock.pack.operation')
1161 package_obj = self.pool.get('stock.quant.package')
1162 stock_move_obj = self.pool.get('stock.move')
1163 for picking_id in picking_ids:
1164 operation_ids = stock_operation_obj.search(cr, uid, [('picking_id', '=', picking_id), ('result_package_id', '=', False)], context=context)
1166 for operation in stock_operation_obj.browse(cr, uid, operation_ids, context=context):
1167 for record in operation.linked_move_operation_ids:
1168 stock_move_obj.check_tracking(cr, uid, record.move_id, operation.package_id.id or operation.lot_id.id, context=context)
1169 package_id = package_obj.create(cr, uid, {}, context=context)
1170 stock_operation_obj.write(cr, uid, operation_ids, {'result_package_id': package_id}, context=context)
1173 def process_product_id_from_ui(self, cr, uid, picking_id, product_id, context=None):
1174 return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id)], context=context)
1176 def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, context=None):
1177 '''This function is called each time there barcode scanner reads an input'''
1178 lot_obj = self.pool.get('stock.production.lot')
1179 package_obj = self.pool.get('stock.quant.package')
1180 product_obj = self.pool.get('product.product')
1181 stock_operation_obj = self.pool.get('stock.pack.operation')
1182 #check if the barcode correspond to a product
1183 matching_product_ids = product_obj.search(cr, uid, ['|', ('ean13', '=', barcode_str), ('default_code', '=', barcode_str)], context=context)
1184 if matching_product_ids:
1185 self.process_product_id_from_ui(cr, uid, picking_id, matching_product_ids[0], context=context)
1187 #check if the barcode correspond to a lot
1188 matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1189 if matching_lot_ids:
1190 lot = lot_obj.browse(cr, uid, matching_lot_ids[0], context=context)
1191 stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], context=context)
1193 #check if the barcode correspond to a package
1194 matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1195 if matching_package_ids:
1196 stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], context=context)
1199 class stock_production_lot(osv.osv):
1200 _name = 'stock.production.lot'
1201 _inherit = ['mail.thread']
1202 _description = 'Lot/Serial'
1204 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number"),
1205 'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"),
1206 'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1207 'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants'),
1208 'create_date': fields.datetime('Creation Date'),
1211 'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1212 'product_id': lambda x, y, z, c: c.get('product_id', False),
1214 _sql_constraints = [
1215 ('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'),
1218 def action_traceability(self, cr, uid, ids, context=None):
1219 """ It traces the information of lots
1220 @param self: The object pointer.
1221 @param cr: A database cursor
1222 @param uid: ID of the user currently logged in
1223 @param ids: List of IDs selected
1224 @param context: A standard dictionary
1225 @return: A dictionary of values
1227 quant_obj = self.pool.get("stock.quant")
1228 quants = quant_obj.search(cr, uid, [('lot_id', 'in', ids)], context=context)
1230 for quant in quant_obj.browse(cr, uid, quants, context=context):
1231 moves |= {move.id for move in quant.history_ids}
1234 'domain': "[('id','in',[" + ','.join(map(str, list(moves))) + "])]",
1235 'name': _('Traceability'),
1236 'view_mode': 'tree,form',
1237 'view_type': 'form',
1238 'context': {'tree_view_ref': 'stock.view_move_tree'},
1239 'res_model': 'stock.move',
1240 'type': 'ir.actions.act_window',
1245 # ----------------------------------------------------
1247 # ----------------------------------------------------
1249 class stock_move(osv.osv):
1250 _name = "stock.move"
1251 _description = "Stock Move"
1252 _order = 'date_expected desc, id'
1255 def get_price_unit(self, cr, uid, move, context=None):
1256 """ Returns the unit price to store on the quant """
1257 return move.price_unit or move.product_id.standard_price
1259 def name_get(self, cr, uid, ids, context=None):
1261 for line in self.browse(cr, uid, ids, context=context):
1262 name = line.location_id.name + ' > ' + line.location_dest_id.name
1263 if line.product_id.code:
1264 name = line.product_id.code + ': ' + name
1265 if line.picking_id.origin:
1266 name = line.picking_id.origin + '/ ' + name
1267 res.append((line.id, name))
1270 def create(self, cr, uid, vals, context=None):
1271 if vals.get('product_id') and not vals.get('price_unit'):
1272 prod_obj = self.pool.get('product.product')
1273 vals['price_unit'] = prod_obj.browse(cr, uid, vals['product_id'], context=context).standard_price
1274 return super(stock_move, self).create(cr, uid, vals, context=context)
1276 def _quantity_normalize(self, cr, uid, ids, name, args, context=None):
1277 uom_obj = self.pool.get('product.uom')
1279 for m in self.browse(cr, uid, ids, context=context):
1280 res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False)
1283 def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
1284 uom_obj = self.pool.get('product.uom')
1286 for move in self.browse(cr, uid, ids, context=context):
1287 qty = move.product_qty
1288 for record in move.linked_move_operation_ids:
1290 #converting the remaining quantity in the move UoM
1291 res[move.id] = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id)
1294 def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
1295 res = dict.fromkeys(ids, False)
1296 for move in self.browse(cr, uid, ids, context=context):
1297 if move.state == 'done':
1298 res[move.id] = [q.id for q in move.quant_ids]
1300 res[move.id] = [q.id for q in move.reserved_quant_ids]
1303 def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
1304 quant_obj = self.pool.get('stock.quant')
1305 res = dict.fromkeys(ids, False)
1306 for move in self.browse(cr, uid, ids, context=context):
1307 if move.state == 'done':
1308 res[move.id] = move.product_qty
1310 sublocation_ids = self.pool.get('stock.location').search(cr, uid, [('id', 'child_of', [move.location_id.id])], context=context)
1311 quant_ids = quant_obj.search(cr, uid, [('location_id', 'in', sublocation_ids), ('product_id', '=', move.product_id.id), ('reservation_id', '=', False)], context=context)
1313 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1314 availability += quant.qty
1315 res[move.id] = min(move.product_qty, availability)
1318 def _get_string_qty_information(self, cr, uid, ids, field_name, args, context=None):
1319 settings_obj = self.pool.get('stock.config.settings')
1320 uom_obj = self.pool.get('product.uom')
1321 res = dict.fromkeys(ids, '')
1322 for move in self.browse(cr, uid, ids, context=context):
1323 if move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal':
1324 res[move.id] = '' # 'not applicable' or 'n/a' could work too
1326 total_available = min(move.product_qty, move.reserved_availability + move.availability)
1327 total_available = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, total_available, move.product_uom.id)
1328 info = str(total_available)
1329 #look in the settings if we need to display the UoM name or not
1330 config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
1332 stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
1333 if stock_settings.group_uom:
1334 info += ' ' + move.product_uom.name
1335 if move.reserved_availability:
1336 if move.reserved_availability != total_available:
1337 #some of the available quantity is assigned and some are available but not reserved
1338 reserved_available = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, move.reserved_availability, move.product_uom.id)
1339 info += _(' (%s reserved)') % str(reserved_available)
1341 #all available quantity is assigned
1342 info += _(' (reserved)')
1346 def _get_reserved_availability(self, cr, uid, ids, field_name, args, context=None):
1347 res = dict.fromkeys(ids, 0)
1348 for move in self.browse(cr, uid, ids, context=context):
1349 res[move.id] = sum([quant.qty for quant in move.reserved_quant_ids])
1352 def _get_move(self, cr, uid, ids, context=None):
1354 for quant in self.browse(cr, uid, ids, context=context):
1355 if quant.reservation_id:
1356 res.add(quant.reservation_id.id)
1359 def _get_move_ids(self, cr, uid, ids, context=None):
1361 for picking in self.browse(cr, uid, ids, context=context):
1362 res += [x.id for x in picking.move_lines]
1366 'name': fields.char('Description', required=True, select=True),
1367 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1368 'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1369 '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)]}),
1370 'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"),
1371 'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}),
1372 # TODO: improve store to add dependency on product UoM
1373 'product_qty': fields.function(_quantity_normalize, type='float', store=True, string='Quantity',
1374 digits_compute=dp.get_precision('Product Unit of Measure'),
1375 help='Quantity in the default UoM of the product'),
1376 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
1377 required=True, states={'done': [('readonly', True)]},
1378 help="This is the quantity of products from an inventory "
1379 "point of view. For moves in the state 'done', this is the "
1380 "quantity of products that were actually moved. For other "
1381 "moves, this is the quantity of product that is planned to "
1382 "be moved. Lowering this quantity does not generate a "
1383 "backorder. Changing this quantity on assigned moves affects "
1384 "the product reservation, and should be done with care."
1386 'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]}),
1387 'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}),
1388 'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1390 'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1392 '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."),
1393 '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."),
1395 # FP Note: should we remove this?
1396 '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"),
1399 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True),
1400 'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
1402 'picking_id': fields.many2one('stock.picking', 'Reference', select=True, states={'done': [('readonly', True)]}),
1403 'picking_priority': fields.related('picking_id', 'priority', type='selection', selection=[('0', 'Low'), ('1', 'Normal'), ('2', 'High')], string='Picking Priority', store={'stock.picking': (_get_move_ids, ['priority'], 10)}),
1404 'note': fields.text('Notes'),
1405 'state': fields.selection([('draft', 'New'),
1406 ('cancel', 'Cancelled'),
1407 ('waiting', 'Waiting Another Move'),
1408 ('confirmed', 'Waiting Availability'),
1409 ('assigned', 'Available'),
1411 ], 'Status', readonly=True, select=True,
1412 help= "* New: When the stock move is created and not yet confirmed.\n"\
1413 "* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"\
1414 "* 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"\
1415 "* Available: When products are reserved, it is set to \'Available\'.\n"\
1416 "* Done: When the shipment is processed, the state is \'Done\'."),
1418 '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
1420 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1421 '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"),
1422 'backorder_id': fields.related('picking_id', 'backorder_id', type='many2one', relation="stock.picking", string="Back Order of", select=True),
1423 'origin': fields.char("Source"),
1424 'procure_method': fields.selection([('make_to_stock', 'Make to Stock'), ('make_to_order', 'Make to Order')], 'Procurement Method', required=True, help="Make to Stock: When needed, the product is taken from the stock or we wait for replenishment. \nMake to Order: When needed, the product is purchased or produced."),
1426 # used for colors in tree views:
1427 'scrapped': fields.related('location_dest_id', 'scrap_location', type='boolean', relation='stock.location', string='Scrapped', readonly=True),
1429 'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Moved Quants'),
1430 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
1431 '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'),
1432 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity',
1433 digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]},),
1434 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1435 'group_id': fields.many2one('procurement.group', 'Procurement Group'),
1436 'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'),
1437 'push_rule_id': fields.many2one('stock.location.path', 'Push Rule', help='The push rule that created this stock move'),
1438 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
1439 'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
1440 'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
1441 'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.quant', string='Lots'),
1442 'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move'),
1443 'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
1444 'reserved_availability': fields.function(_get_reserved_availability, type='float', string='Quantity Reserved', readonly=True, help='Quantity that has already been reserved for this move'),
1445 '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'),
1446 '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'),
1447 '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'"),
1448 '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'"),
1449 'putaway_ids': fields.one2many('stock.move.putaway', 'move_id', 'Put Away Suggestions'),
1450 '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"),
1451 '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)."),
1454 def _default_location_destination(self, cr, uid, context=None):
1455 context = context or {}
1456 if context.get('default_picking_type_id', False):
1457 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1458 return pick_type.default_location_dest_id and pick_type.default_location_dest_id.id or False
1461 def _default_location_source(self, cr, uid, context=None):
1462 context = context or {}
1463 if context.get('default_picking_type_id', False):
1464 pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1465 return pick_type.default_location_src_id and pick_type.default_location_src_id.id or False
1468 def _default_destination_address(self, cr, uid, context=None):
1469 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1470 return user.company_id.partner_id.id
1473 'location_id': _default_location_source,
1474 'location_dest_id': _default_location_destination,
1475 'partner_id': _default_destination_address,
1479 'product_uom_qty': 1.0,
1481 'date': fields.datetime.now,
1482 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1483 'date_expected': fields.datetime.now,
1484 'procure_method': 'make_to_stock',
1488 def _check_uom(self, cr, uid, ids, context=None):
1489 for move in self.browse(cr, uid, ids, context=context):
1490 if move.product_id.uom_id.category_id.id != move.product_uom.category_id.id:
1496 '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.',
1499 def copy_data(self, cr, uid, id, default=None, context=None):
1502 default = default.copy()
1503 default['move_orig_ids'] = []
1504 default['quant_ids'] = []
1505 default['move_dest_id'] = False
1506 default['reserved_quant_ids'] = []
1507 default['returned_move_ids'] = []
1508 default['linked_move_operation_ids'] = []
1509 if not default.get('origin_returned_move_id'):
1510 default['origin_returned_move_id'] = False
1511 default['state'] = 'draft'
1512 return super(stock_move, self).copy_data(cr, uid, id, default, context)
1514 def do_unreserve(self, cr, uid, move_ids, context=None):
1515 quant_obj = self.pool.get("stock.quant")
1516 for move in self.browse(cr, uid, move_ids, context=context):
1517 if move.state in ('done', 'cancel'):
1518 raise osv.except_osv(_('Operation Forbidden!'), _('Cannot unreserve a done move'))
1519 quant_obj.quants_unreserve(cr, uid, move, context=context)
1521 for putaway_rec in move.putaway_ids:
1522 putaway_values.append((2, putaway_rec.id))
1523 self.write(cr, uid, [move.id], {'state': 'confirmed', 'putaway_ids': putaway_values}, context=context)
1525 def _prepare_procurement_from_move(self, cr, uid, move, context=None):
1526 origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or "/")
1527 group_id = move.group_id and move.group_id.id or False
1529 if move.rule_id.group_propagation_option == 'fixed' and move.rule_id.group_id:
1530 group_id = move.rule_id.group_id.id
1531 elif move.rule_id.group_propagation_option == 'none':
1534 'name': move.rule_id and move.rule_id.name or "/",
1536 'company_id': move.company_id and move.company_id.id or False,
1537 'date_planned': move.date,
1538 'product_id': move.product_id.id,
1539 'product_qty': move.product_qty,
1540 'product_uom': move.product_uom.id,
1541 'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_qty,
1542 'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
1543 'location_id': move.location_id.id,
1544 'move_dest_id': move.id,
1545 'group_id': group_id,
1546 'route_ids': [(4, x.id) for x in move.route_ids],
1547 'warehouse_id': move.warehouse_id and move.warehouse_id.id or False,
1550 def _push_apply(self, cr, uid, moves, context=None):
1551 push_obj = self.pool.get("stock.location.path")
1553 #1) if the move is already chained, there is no need to check push rules
1554 #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
1555 # to receive goods without triggering the push rules again (which would duplicate chained operations)
1556 if not move.move_dest_id and not move.origin_returned_move_id:
1557 domain = [('location_from_id', '=', move.location_dest_id.id)]
1558 if move.warehouse_id:
1559 domain += ['|', ('warehouse_id', '=', move.warehouse_id.id), ('warehouse_id', '=', False)]
1560 #priority goes to the route defined on the product and product category
1561 route_ids = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids]
1562 rules = push_obj.search(cr, uid, domain + [('route_id', 'in', route_ids)], order='route_sequence, sequence', context=context)
1564 #but if there's no rule matching, we try without filtering on routes
1565 rules = push_obj.search(cr, uid, domain, order='route_sequence, sequence', context=context)
1567 rule = push_obj.browse(cr, uid, rules[0], context=context)
1568 push_obj._apply(cr, uid, rule, move, context=context)
1571 # Create the stock.move.putaway records
1572 def _putaway_apply(self, cr, uid, move, putaway, context=None):
1573 # Should call different methods here in later versions
1574 moveputaway_obj = self.pool.get('stock.move.putaway')
1575 quant_obj = self.pool.get('stock.quant')
1576 if putaway.method == 'fixed' and putaway.location_spec_id:
1577 qty = move.product_qty
1578 for row in quant_obj.read_group(cr, uid, [('reservation_id', '=', move.id)], ['qty', 'lot_id'], ['lot_id'], context=context):
1581 'location_id': putaway.location_spec_id.id,
1582 'quantity': row['qty'],
1583 'lot_id': row.get('lot_id') and row['lot_id'][0] or False,
1585 moveputaway_obj.create(cr, SUPERUSER_ID, vals, context=context)
1588 #if the quants assigned aren't fully explaining where the products have to be moved
1592 'location_id': putaway.location_spec_id.id,
1595 moveputaway_obj.create(cr, SUPERUSER_ID, vals, context=context)
1597 def _putaway_check(self, cr, uid, ids, context=None):
1598 for move in self.browse(cr, uid, ids, context=context):
1599 putaway = self.pool.get('stock.location').get_putaway_strategy(cr, uid, move.location_dest_id, move.product_id, context=context)
1601 self._putaway_apply(cr, uid, move, putaway, context=context)
1603 def _create_procurement(self, cr, uid, move, context=None):
1604 """ This will create a procurement order """
1605 return self.pool.get("procurement.order").create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
1607 def write(self, cr, uid, ids, vals, context=None):
1610 if isinstance(ids, (int, long)):
1612 # Check that we do not modify a stock.move which is done
1613 frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1614 for move in self.browse(cr, uid, ids, context=context):
1615 if move.state == 'done':
1616 if frozen_fields.intersection(vals):
1617 raise osv.except_osv(_('Operation Forbidden!'),
1618 _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
1619 propagated_changes_dict = {}
1620 #propagation of quantity change
1621 if vals.get('product_uom_qty'):
1622 propagated_changes_dict['product_uom_qty'] = vals['product_uom_qty']
1623 if vals.get('product_uom_id'):
1624 propagated_changes_dict['product_uom_id'] = vals['product_uom_id']
1625 #propagation of expected date:
1626 propagated_date_field = False
1627 if vals.get('date_expected'):
1628 #propagate any manual change of the expected date
1629 propagated_date_field = 'date_expected'
1630 elif (vals.get('state', '') == 'done' and vals.get('date')):
1631 #propagate also any delta observed when setting the move as done
1632 propagated_date_field = 'date'
1634 if not context.get('do_not_propagate', False) and (propagated_date_field or propagated_changes_dict):
1635 #any propagation is (maybe) needed
1636 for move in self.browse(cr, uid, ids, context=context):
1637 if move.move_dest_id and move.propagate:
1638 if 'date_expected' in propagated_changes_dict:
1639 propagated_changes_dict.pop('date_expected')
1640 if propagated_date_field:
1641 current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
1642 new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT)
1643 delta = new_date - current_date
1644 if abs(delta.days) >= move.company_id.propagation_minimum_delta:
1645 old_move_date = datetime.strptime(move.move_dest_id.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
1646 new_move_date = (old_move_date + relativedelta.relativedelta(days=delta.days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1647 propagated_changes_dict['date_expected'] = new_move_date
1648 #For pushed moves as well as for pulled moves, propagate by recursive call of write().
1649 #Note that, for pulled moves we intentionally don't propagate on the procurement.
1650 if propagated_changes_dict:
1651 self.write(cr, uid, [move.move_dest_id.id], propagated_changes_dict, context=context)
1652 return super(stock_move, self).write(cr, uid, ids, vals, context=context)
1654 def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1655 """ On change of product quantity finds UoM and UoS quantities
1656 @param product_id: Product id
1657 @param product_qty: Changed Quantity of product
1658 @param product_uom: Unit of measure of product
1659 @param product_uos: Unit of sale of product
1660 @return: Dictionary of values
1663 'product_uos_qty': 0.00
1667 if (not product_id) or (product_qty <= 0.0):
1668 result['product_qty'] = 0.0
1669 return {'value': result}
1671 product_obj = self.pool.get('product.product')
1672 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1674 # Warn if the quantity was decreased
1676 for move in self.read(cr, uid, ids, ['product_qty']):
1677 if product_qty < move['product_qty']:
1679 'title': _('Information'),
1680 'message': _("By changing this quantity here, you accept the "
1681 "new quantity as complete: OpenERP will not "
1682 "automatically generate a back order.")})
1685 if product_uos and product_uom and (product_uom != product_uos):
1686 result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1688 result['product_uos_qty'] = product_qty
1690 return {'value': result, 'warning': warning}
1692 def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1693 product_uos, product_uom):
1694 """ On change of product quantity finds UoM and UoS quantities
1695 @param product_id: Product id
1696 @param product_uos_qty: Changed UoS Quantity of product
1697 @param product_uom: Unit of measure of product
1698 @param product_uos: Unit of sale of product
1699 @return: Dictionary of values
1702 'product_uom_qty': 0.00
1706 if (not product_id) or (product_uos_qty <= 0.0):
1707 result['product_uos_qty'] = 0.0
1708 return {'value': result}
1710 product_obj = self.pool.get('product.product')
1711 uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1713 # Warn if the quantity was decreased
1714 for move in self.read(cr, uid, ids, ['product_uos_qty']):
1715 if product_uos_qty < move['product_uos_qty']:
1717 'title': _('Warning: No Back Order'),
1718 'message': _("By changing the quantity here, you accept the "
1719 "new quantity as complete: OpenERP will not "
1720 "automatically generate a Back Order.")})
1723 if product_uos and product_uom and (product_uom != product_uos):
1724 result['product_uom_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1726 result['product_uom_qty'] = product_uos_qty
1727 return {'value': result, 'warning': warning}
1729 def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, partner_id=False):
1730 """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
1731 @param prod_id: Changed Product id
1732 @param loc_id: Source location id
1733 @param loc_dest_id: Destination location id
1734 @param partner_id: Address id of partner
1735 @return: Dictionary of values
1739 user = self.pool.get('res.users').browse(cr, uid, uid)
1740 lang = user and user.lang or False
1742 addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id)
1744 lang = addr_rec and addr_rec.lang or False
1745 ctx = {'lang': lang}
1747 product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1748 uos_id = product.uos_id and product.uos_id.id or False
1750 'product_uom': product.uom_id.id,
1751 'product_uos': uos_id,
1752 'product_uom_qty': 1.00,
1753 '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'],
1756 result['name'] = product.partner_ref
1758 result['location_id'] = loc_id
1760 result['location_dest_id'] = loc_dest_id
1761 return {'value': result}
1763 def _picking_assign(self, cr, uid, move, context=None):
1764 if move.picking_id or not move.picking_type_id:
1766 context = context or {}
1767 pick_obj = self.pool.get("stock.picking")
1769 group = move.group_id and move.group_id.id or False
1770 picks = pick_obj.search(cr, uid, [
1771 ('group_id', '=', group),
1772 ('location_id', '=', move.location_id.id),
1773 ('location_dest_id', '=', move.location_dest_id.id),
1774 ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context)
1779 'origin': move.origin,
1780 'company_id': move.company_id and move.company_id.id or False,
1781 'move_type': move.group_id and move.group_id.move_type or 'one',
1782 'partner_id': move.group_id and move.group_id.partner_id and move.group_id.partner_id.id or False,
1783 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
1785 pick = pick_obj.create(cr, uid, values, context=context)
1786 move.write({'picking_id': pick})
1789 def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
1790 """ On change of Scheduled Date gives a Move date.
1791 @param date_expected: Scheduled Date
1792 @param date: Move Date
1795 if not date_expected:
1796 date_expected = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1797 return {'value': {'date': date_expected}}
1799 def action_confirm(self, cr, uid, ids, context=None):
1800 """ Confirms stock move or put it in waiting if it's linked to another move.
1801 @return: List of ids.
1803 if isinstance(ids, (int, long)):
1809 for move in self.browse(cr, uid, ids, context=context):
1811 #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)
1812 if move.move_orig_ids:
1814 #if the move is split and some of the ancestor was preceeded, then it's waiting as well
1815 elif move.split_from:
1816 move2 = move.split_from
1817 while move2 and state != 'waiting':
1818 if move2.move_orig_ids:
1820 move2 = move2.split_from
1822 states[state].append(move.id)
1823 self._picking_assign(cr, uid, move, context=context)
1825 for move in self.browse(cr, uid, states['confirmed'], context=context):
1826 if move.procure_method == 'make_to_order':
1827 self._create_procurement(cr, uid, move, context=context)
1828 states['waiting'].append(move.id)
1829 states['confirmed'].remove(move.id)
1831 for state, write_ids in states.items():
1833 self.write(cr, uid, write_ids, {'state': state})
1834 moves = self.browse(cr, uid, ids, context=context)
1835 self._push_apply(cr, uid, moves, context=context)
1838 def force_assign(self, cr, uid, ids, context=None):
1839 """ Changes the state to assigned.
1842 #check putaway method
1843 self._putaway_check(cr, uid, ids, context=context)
1844 return self.write(cr, uid, ids, {'state': 'assigned'})
1846 def check_tracking(self, cr, uid, move, lot_id, context=None):
1847 """ Checks if serial number is assigned to stock move or not and raise an error if it had to.
1850 if move.product_id.track_all and not move.location_dest_id.usage == 'inventory':
1852 elif move.product_id.track_incoming and move.location_id.usage in ('supplier', 'transit', 'inventory') and move.location_dest_id.usage == 'internal':
1854 elif move.product_id.track_outgoing and move.location_dest_id.usage in ('customer', 'transit') and move.location_id.usage == 'internal':
1856 if check and not lot_id:
1857 raise osv.except_osv(_('Warning!'), _('You must assign a serial number for the product %s') % (move.product_id.name))
1859 def action_assign(self, cr, uid, ids, context=None):
1860 """ Checks the product type and accordingly writes the state.
1862 context = context or {}
1863 quant_obj = self.pool.get("stock.quant")
1864 to_assign_moves = []
1865 prefered_domain = {}
1866 fallback_domain = {}
1870 for move in self.browse(cr, uid, ids, context=context):
1871 if move.state not in ('confirmed', 'waiting', 'assigned'):
1873 if move.picking_type_id and move.picking_type_id.auto_force_assign:
1874 to_assign_moves.append(move.id)
1875 #in case the move is returned, we want to try to find quants before forcing the assignment
1876 if not move.origin_returned_move_id:
1878 if move.product_id.type == 'consu':
1879 to_assign_moves.append(move.id)
1882 todo_moves.append(move)
1884 #we always keep the quants already assigned and try to find the remaining quantity on quants not assigned only
1885 main_domain[move.id] = [('reservation_id', '=', False), ('qty', '>', 0)]
1887 #if the move is preceeded, restrict the choice of quants in the ones moved previously in original move
1891 move_orig_ids += [x.id for x in move2.move_orig_ids]
1892 #loop on the split_from to find the ancestor of split moves only if the move has not direct ancestor (priority goes to them)
1893 move2 = not move2.move_orig_ids and move2.split_from or False
1895 main_domain[move.id] += [('history_ids', 'in', move_orig_ids)]
1897 #if the move is returned from another, restrict the choice of quants to the ones that follow the returned move
1898 if move.origin_returned_move_id:
1899 main_domain[move.id] += [('history_ids', 'in', move.origin_returned_move_id.id)]
1900 for link in move.linked_move_operation_ids:
1901 operations.add(link.operation_id)
1902 # Check all ops and sort them: we want to process first the packages, then operations with lot then the rest
1903 operations = list(operations)
1904 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))
1905 for ops in operations:
1906 #first try to find quants based on specific domains given by linked operations
1907 for record in ops.linked_move_operation_ids:
1908 move = record.move_id
1909 domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
1910 qty_already_assigned = sum([q.qty for q in record.reserved_quant_ids])
1911 qty = record.qty - qty_already_assigned
1912 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=domain, prefered_domain=[], fallback_domain=[], restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
1913 quant_obj.quants_reserve(cr, uid, quants, move, record, context=context)
1915 for move in todo_moves:
1916 #then if the move isn't totally assigned, try to find quants without any specific domain
1917 if move.state != 'assigned':
1918 qty_already_assigned = move.reserved_availability
1919 qty = move.product_qty - qty_already_assigned
1920 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain[move.id], prefered_domain=[], fallback_domain=[], restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
1921 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
1923 #force assignation of consumable products and picking type auto_force_assign
1925 self.force_assign(cr, uid, to_assign_moves, context=context)
1926 #check if a putaway rule is likely to be processed and store result on the move
1927 self._putaway_check(cr, uid, ids, context=context)
1929 def action_cancel(self, cr, uid, ids, context=None):
1930 """ Cancels the moves and if all moves are cancelled it cancels the picking.
1933 procurement_obj = self.pool.get('procurement.order')
1934 context = context or {}
1935 for move in self.browse(cr, uid, ids, context=context):
1936 if move.state == 'done':
1937 raise osv.except_osv(_('Operation Forbidden!'),
1938 _('You cannot cancel a stock move that has been set to \'Done\'.'))
1939 if move.reserved_quant_ids:
1940 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
1941 if context.get('cancel_procurement'):
1943 procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
1944 procurement_obj.cancel(cr, uid, procurement_ids, context=context)
1945 elif move.move_dest_id:
1946 #cancel chained moves
1948 self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
1949 elif move.move_dest_id.state == 'waiting':
1950 self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'})
1951 return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1953 def _check_package_from_moves(self, cr, uid, ids, context=None):
1954 pack_obj = self.pool.get("stock.quant.package")
1956 for move in self.browse(cr, uid, ids, context=context):
1957 packs |= set([q.package_id.id for q in move.quant_ids if q.package_id and q.qty > 0])
1958 return pack_obj._check_location_constraint(cr, uid, list(packs), context=context)
1960 def action_done(self, cr, uid, ids, context=None):
1961 """ Process completly the moves given as ids and if all moves are done, it will finish the picking.
1963 context = context or {}
1964 picking_obj = self.pool.get("stock.picking")
1965 quant_obj = self.pool.get("stock.quant")
1966 pack_op_obj = self.pool.get("stock.pack.operation")
1967 todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
1969 ids = self.action_confirm(cr, uid, todo, context=context)
1971 procurement_ids = []
1972 #Search operations that are linked to the moves
1975 for move in self.browse(cr, uid, ids, context=context):
1976 move_qty[move.id] = move.product_qty
1977 for link in move.linked_move_operation_ids:
1978 operations.add(link.operation_id)
1980 #Sort operations according to entire packages first, then package + lot, package only, lot only
1981 operations = list(operations)
1982 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))
1984 for ops in operations:
1986 pickings.add(ops.picking_id.id)
1987 main_domain = [('qty', '>', 0)]
1988 for record in ops.linked_move_operation_ids:
1989 move = record.move_id
1990 prefered_domain = [('reservation_id', '=', move.id)]
1991 fallback_domain = [('reservation_id', '=', False)]
1992 self.check_tracking(cr, uid, move, ops.package_id.id or ops.lot_id.id, context=context)
1993 dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
1994 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, record.qty, domain=dom, prefered_domain=prefered_domain,
1995 fallback_domain=fallback_domain, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
1997 if not record.operation_id.package_id:
1998 #if a package and a result_package is given, we don't enter here because it will be processed by process_packaging() later
1999 #but for operations having only result_package_id, we will create new quants in the final package directly
2000 package_id = record.operation_id.result_package_id.id or False
2001 quant_obj.quants_move(cr, uid, quants, move, lot_id=ops.lot_id.id, owner_id=ops.owner_id.id, src_package_id=ops.package_id.id, dest_package_id=package_id, context=context)
2003 pack_op_obj.process_packaging(cr, uid, ops, [x[0].id for x in quants if x[0]], context=context)
2004 move_qty[move.id] -= record.qty
2005 #Check for remaining qtys and unreserve/check move_dest_id in
2006 for move in self.browse(cr, uid, ids, context=context):
2007 if move_qty[move.id] > 0: #(=In case no pack operations in picking)
2008 main_domain = [('qty', '>', 0)]
2009 prefered_domain = [('reservation_id', '=', move.id)]
2010 fallback_domain = [('reservation_id', '=', False)]
2011 self.check_tracking(cr, uid, move, move.restrict_lot_id.id, context=context)
2012 qty = move_qty[move.id]
2013 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain, prefered_domain=prefered_domain, fallback_domain=fallback_domain, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2014 quant_obj.quants_move(cr, uid, quants, move, lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id, context=context)
2015 #unreserve the quants and make them available for other operations/moves
2016 quant_obj.quants_unreserve(cr, uid, move, context=context)
2018 #Check moves that were pushed
2019 if move.move_dest_id.state in ('waiting', 'confirmed'):
2020 other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
2021 ('move_dest_id', '=', move.move_dest_id.id)], context=context)
2022 #If no other moves for the move that got pushed:
2023 if not other_upstream_move_ids and move.move_dest_id.state in ('waiting', 'confirmed'):
2024 self.action_assign(cr, uid, [move.move_dest_id.id], context=context)
2025 if move.procurement_id:
2026 procurement_ids.append(move.procurement_id.id)
2028 # Check the packages have been placed in the correct locations
2029 self._check_package_from_moves(cr, uid, ids, context=context)
2031 self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2032 self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
2033 #check picking state to set the date_done is needed
2035 for picking in picking_obj.browse(cr, uid, list(pickings), context=context):
2036 if picking.state == 'done' and not picking.date_done:
2037 done_picking.append(picking.id)
2039 picking_obj.write(cr, uid, done_picking, {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2042 def unlink(self, cr, uid, ids, context=None):
2043 context = context or {}
2044 for move in self.browse(cr, uid, ids, context=context):
2045 if move.state not in ('draft', 'cancel'):
2046 raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
2047 return super(stock_move, self).unlink(cr, uid, ids, context=context)
2049 def action_scrap(self, cr, uid, ids, quantity, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
2050 """ Move the scrap/damaged product into scrap location
2051 @param cr: the database cursor
2052 @param uid: the user id
2053 @param ids: ids of stock move object to be scrapped
2054 @param quantity : specify scrap qty
2055 @param location_id : specify scrap location
2056 @param context: context arguments
2057 @return: Scraped lines
2059 #quantity should be given in MOVE UOM
2061 raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
2063 for move in self.browse(cr, uid, ids, context=context):
2064 source_location = move.location_id
2065 if move.state == 'done':
2066 source_location = move.location_dest_id
2067 #Previously used to prevent scraping from virtual location but not necessary anymore
2068 #if source_location.usage != 'internal':
2069 #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
2070 #raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
2071 move_qty = move.product_qty
2072 uos_qty = quantity / move_qty * move.product_uos_qty
2074 'location_id': source_location.id,
2075 'product_uom_qty': quantity,
2076 'product_uos_qty': uos_qty,
2077 'state': move.state,
2079 'location_dest_id': location_id,
2080 'restrict_lot_id': restrict_lot_id,
2081 'restrict_partner_id': restrict_partner_id,
2083 new_move = self.copy(cr, uid, move.id, default_val)
2086 product_obj = self.pool.get('product.product')
2087 for product in product_obj.browse(cr, uid, [move.product_id.id], context=context):
2089 uom = product.uom_id.name if product.uom_id else ''
2090 message = _("%s %s %s has been <b>moved to</b> scrap.") % (quantity, uom, product.name)
2091 move.picking_id.message_post(body=message)
2093 self.action_done(cr, uid, res, context=context)
2096 def split(self, cr, uid, move, qty, restrict_lot_id=False, restrict_partner_id=False, context=None):
2097 """ Splits qty from move move into a new move
2098 :param move: browse record
2099 :param qty: float. quantity to split (given in product UoM)
2100 :param context: dictionay. can contains the special key 'source_location_id' in order to force the source location when copying the move
2102 returns the ID of the backorder move created
2104 if move.state in ('done', 'cancel'):
2105 raise osv.except_osv(_('Error'), _('You cannot split a move done'))
2106 if move.state == 'draft':
2107 #we restrict the split of a draft move because if not confirmed yet, it may be replaced by several other moves in
2108 #case of phantom bom (with mrp module). And we don't want to deal with this complexity by copying the product that will explode.
2109 raise osv.except_osv(_('Error'), _('You cannot split a draft move. It needs to be confirmed first.'))
2111 if move.product_qty <= qty or qty == 0:
2114 uom_obj = self.pool.get('product.uom')
2115 context = context or {}
2117 uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id)
2118 uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
2121 'product_uom_qty': uom_qty,
2122 'product_uos_qty': uos_qty,
2123 'state': move.state,
2124 'procure_method': 'make_to_stock',
2125 'restrict_lot_id': restrict_lot_id,
2126 'restrict_partner_id': restrict_partner_id,
2127 'split_from': move.id,
2129 if context.get('source_location_id'):
2130 defaults['location_id'] = context['source_location_id']
2131 new_move = self.copy(cr, uid, move.id, defaults)
2133 ctx = context.copy()
2134 ctx['do_not_propagate'] = True
2135 self.write(cr, uid, [move.id], {
2136 'product_uom_qty': move.product_uom_qty - uom_qty,
2137 'product_uos_qty': move.product_uos_qty - uos_qty,
2140 if move.move_dest_id and move.propagate:
2141 new_move_prop = self.split(cr, uid, move.move_dest_id, qty, context=context)
2142 self.write(cr, uid, [new_move], {'move_dest_id': new_move_prop}, context=context)
2143 #returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
2144 #thus the result of action_confirm should always be a list of 1 element length)
2145 return self.action_confirm(cr, uid, [new_move], context=context)[0]
2148 class stock_inventory(osv.osv):
2149 _name = "stock.inventory"
2150 _description = "Inventory"
2152 def _get_move_ids_exist(self, cr, uid, ids, field_name, arg, context=None):
2154 for inv in self.browse(cr, uid, ids, context=context):
2160 def _get_available_filters(self, cr, uid, context=None):
2162 This function will return the list of filter allowed according to the options checked
2163 in 'Settings\Warehouse'.
2165 :rtype: list of tuple
2167 #default available choices
2168 res_filter = [('none', _('All products of a whole location')), ('product', _('One product only'))]
2169 settings_obj = self.pool.get('stock.config.settings')
2170 config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
2171 #If we don't have updated config until now, all fields are by default false and so should be not dipslayed
2175 stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
2176 if stock_settings.group_stock_tracking_owner:
2177 res_filter.append(('owner', _('One owner only')))
2178 res_filter.append(('product_owner', _('One product for a specific owner')))
2179 if stock_settings.group_stock_tracking_lot:
2180 res_filter.append(('lot', _('One Lot/Serial Number')))
2181 if stock_settings.group_stock_packaging:
2182 res_filter.append(('pack', _('A Pack')))
2186 'name': fields.char('Inventory Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
2187 'date': fields.datetime('Inventory Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Create Date."),
2188 'date_done': fields.datetime('Date done', help="Inventory Validation Date."),
2189 'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines."),
2190 'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves.", states={'done': [('readonly', True)]}),
2191 'state': fields.selection([('draft', 'Draft'), ('cancel', 'Cancelled'), ('confirm', 'In Progress'), ('done', 'Validated')], 'Status', readonly=True, select=True),
2192 'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
2193 'location_id': fields.many2one('stock.location', 'Location', required=True, readonly=True, states={'draft': [('readonly', False)]}),
2194 'product_id': fields.many2one('product.product', 'Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
2195 'package_id': fields.many2one('stock.quant.package', 'Pack', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Pack to focus your inventory on a particular Pack."),
2196 'partner_id': fields.many2one('res.partner', 'Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
2197 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number."),
2198 'move_ids_exist': fields.function(_get_move_ids_exist, type='boolean', string=' Stock Move Exists?', help='technical field for attrs in view'),
2199 'filter': fields.selection(_get_available_filters, 'Selection Filter'),
2202 def _default_stock_location(self, cr, uid, context=None):
2204 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2205 return warehouse.lot_stock_id.id
2210 'date': fields.datetime.now,
2212 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2213 'location_id': _default_stock_location,
2216 def set_checked_qty(self, cr, uid, ids, context=None):
2217 inventory = self.browse(cr, uid, ids[0], context=context)
2218 line_ids = [line.id for line in inventory.line_ids]
2219 self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
2222 def copy(self, cr, uid, id, default=None, context=None):
2225 default = default.copy()
2226 default.update({'move_ids': [], 'date_done': False})
2227 return super(stock_inventory, self).copy(cr, uid, id, default, context=context)
2229 def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2230 """ Creates a stock move from an inventory line
2231 @param inventory_line:
2235 return self.pool.get('stock.move').create(cr, uid, move_vals)
2237 def action_done(self, cr, uid, ids, context=None):
2238 """ Finish the inventory
2243 move_obj = self.pool.get('stock.move')
2244 for inv in self.browse(cr, uid, ids, context=context):
2245 for inventory_line in inv.line_ids:
2246 if inventory_line.product_qty < 0 and inventory_line.product_qty != inventory_line.th_qty:
2247 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)))
2248 if not inv.move_ids:
2249 self.action_check(cr, uid, [inv.id], context=context)
2251 #the action_done on stock_move has to be done in 2 steps:
2252 #first, we start moving the products from stock to inventory loss
2253 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage == 'internal'], context=context)
2254 #then, we move from inventory loss. This 2 steps process is needed because some moved quant may need to be put again in stock
2255 move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage != 'internal'], context=context)
2256 self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2259 def _create_stock_move(self, cr, uid, inventory, todo_line, context=None):
2260 stock_move_obj = self.pool.get('stock.move')
2261 product_obj = self.pool.get('product.product')
2262 inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id
2264 'name': _('INV:') + (inventory.name or ''),
2265 'product_id': todo_line['product_id'],
2266 'product_uom': todo_line['product_uom_id'],
2267 'date': inventory.date,
2268 'company_id': inventory.company_id.id,
2269 'inventory_id': inventory.id,
2270 'state': 'assigned',
2271 'restrict_lot_id': todo_line.get('prod_lot_id'),
2272 'restrict_partner_id': todo_line.get('partner_id'),
2275 if todo_line['product_qty'] < 0:
2276 #found more than expected
2277 vals['location_id'] = inventory_location_id
2278 vals['location_dest_id'] = todo_line['location_id']
2279 vals['product_uom_qty'] = -todo_line['product_qty']
2281 #found less than expected
2282 vals['location_id'] = todo_line['location_id']
2283 vals['location_dest_id'] = inventory_location_id
2284 vals['product_uom_qty'] = todo_line['product_qty']
2285 return stock_move_obj.create(cr, uid, vals, context=context)
2287 def action_check(self, cr, uid, ids, context=None):
2288 """ Checks the inventory and computes the stock move to do
2291 inventory_line_obj = self.pool.get('stock.inventory.line')
2292 stock_move_obj = self.pool.get('stock.move')
2293 for inventory in self.browse(cr, uid, ids, context=context):
2294 #first remove the existing stock moves linked to this inventory
2295 move_ids = [move.id for move in inventory.move_ids]
2296 stock_move_obj.unlink(cr, uid, move_ids, context=context)
2297 #compute what should be in the inventory lines
2298 theorical_lines = self._get_inventory_lines(cr, uid, inventory, context=context)
2299 for line in inventory.line_ids:
2300 #compare the inventory lines to the theorical ones and store the diff in theorical_lines
2301 inventory_line_obj._resolve_inventory_line(cr, uid, line, theorical_lines, context=context)
2302 #each theorical_lines where product_qty is not 0 is a difference for which we need to create a stock move
2303 for todo_line in theorical_lines:
2304 if todo_line['product_qty'] != 0:
2305 self._create_stock_move(cr, uid, inventory, todo_line, context=context)
2307 def action_cancel_draft(self, cr, uid, ids, context=None):
2308 """ Cancels the stock move and change inventory state to draft.
2311 for inv in self.browse(cr, uid, ids, context=context):
2312 self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2313 self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
2316 def action_cancel_inventory(self, cr, uid, ids, context=None):
2317 self.action_cancel_draft(cr, uid, ids, context=context)
2319 def prepare_inventory(self, cr, uid, ids, context=None):
2320 inventory_line_obj = self.pool.get('stock.inventory.line')
2321 for inventory in self.browse(cr, uid, ids, context=context):
2322 #clean the existing inventory lines before redoing an inventory proposal
2323 line_ids = [line.id for line in inventory.line_ids]
2324 inventory_line_obj.unlink(cr, uid, line_ids, context=context)
2325 #compute the inventory lines and create them
2326 vals = self._get_inventory_lines(cr, uid, inventory, context=context)
2327 for product_line in vals:
2328 inventory_line_obj.create(cr, uid, product_line, context=context)
2329 return self.write(cr, uid, ids, {'state': 'confirm'})
2331 def _get_inventory_lines(self, cr, uid, inventory, context=None):
2332 location_obj = self.pool.get('stock.location')
2333 product_obj = self.pool.get('product.product')
2334 location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
2335 domain = ' location_id in %s'
2336 args = (tuple(location_ids),)
2337 if inventory.partner_id:
2338 domain += ' and owner_id = %s'
2339 args += (inventory.partner_id.id,)
2340 if inventory.lot_id:
2341 domain += ' and lot_id = %s'
2342 args += (inventory.lot_id.id,)
2343 if inventory.product_id:
2344 domain += 'and product_id = %s'
2345 args += (inventory.product_id.id,)
2346 if inventory.package_id:
2347 domain += ' and package_id = %s'
2348 args += (inventory.package_id.id,)
2350 SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
2351 FROM stock_quant WHERE''' + domain + '''
2352 GROUP BY product_id, location_id, lot_id, package_id, partner_id
2355 for product_line in cr.dictfetchall():
2356 #replace the None the dictionary by False, because falsy values are tested later on
2357 for key, value in product_line.items():
2359 product_line[key] = False
2360 product_line['inventory_id'] = inventory.id
2361 product_line['th_qty'] = product_line['product_qty']
2362 if product_line['product_id']:
2363 product = product_obj.browse(cr, uid, product_line['product_id'], context=context)
2364 product_line['product_uom_id'] = product.uom_id.id
2365 vals.append(product_line)
2368 class stock_inventory_line(osv.osv):
2369 _name = "stock.inventory.line"
2370 _description = "Inventory Line"
2371 _rec_name = "inventory_id"
2372 _order = "inventory_id, location_name, product_code, product_name, prod_lot_id"
2374 def _get_product_name_change(self, cr, uid, ids, context=None):
2375 return self.pool.get('stock.inventory.line').search(cr, uid, [('product_id', 'in', ids)], context=context)
2377 def _get_location_change(self, cr, uid, ids, context=None):
2378 return self.pool.get('stock.inventory.line').search(cr, uid, [('location_id', 'in', ids)], context=context)
2381 'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2382 'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
2383 'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2384 'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
2385 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
2386 'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
2387 'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
2388 'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
2389 'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
2390 'th_qty': fields.float('Theoretical Quantity', readonly=True),
2391 'partner_id': fields.many2one('res.partner', 'Owner'),
2392 'product_name': fields.related('product_id', 'name', type='char', string='Product name', store={
2393 'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
2394 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
2395 'product_code': fields.related('product_id', 'default_code', type='char', string='Product code', store={
2396 'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
2397 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
2398 'location_name': fields.related('location_id', 'complete_name', type='char', string='Location name', store={
2399 'stock.location': (_get_location_change, ['name', 'location_id', 'active'], 20),
2400 'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id'], 20),}),
2407 def _resolve_inventory_line(self, cr, uid, inventory_line, theorical_lines, context=None):
2408 #TODO : package_id management !
2410 uom_obj = self.pool.get('product.uom')
2411 for th_line in theorical_lines:
2412 #We try to match the inventory line with a theorical line with same product, lot, location and owner
2413 if th_line['location_id'] == inventory_line.location_id.id and th_line['product_id'] == inventory_line.product_id.id and th_line['prod_lot_id'] == inventory_line.prod_lot_id.id and th_line['partner_id'] == inventory_line.partner_id.id:
2414 uom_reference = inventory_line.product_id.uom_id
2415 real_qty = uom_obj._compute_qty_obj(cr, uid, inventory_line.product_uom_id, inventory_line.product_qty, uom_reference)
2416 th_line['product_qty'] -= real_qty
2419 #if it was still not found, we add it to the theorical lines so that it will create a stock move for it
2422 'inventory_id': inventory_line.inventory_id.id,
2423 'location_id': inventory_line.location_id.id,
2424 'product_id': inventory_line.product_id.id,
2425 'product_uom_id': inventory_line.product_id.uom_id.id,
2426 'product_qty': -inventory_line.product_qty,
2427 'prod_lot_id': inventory_line.prod_lot_id.id,
2428 'partner_id': inventory_line.partner_id.id,
2430 theorical_lines.append(vals)
2432 def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False, owner_id=False, lot_id=False, package_id=False, context=None):
2433 """ Changes UoM and name if product_id changes.
2434 @param location_id: Location id
2435 @param product: Changed product_id
2436 @param uom: UoM product
2437 @return: Dictionary of changed values
2439 context = context or {}
2441 return {'value': {'product_qty': 0.0, 'product_uom_id': False}}
2442 uom_obj = self.pool.get('product.uom')
2443 ctx = context.copy()
2444 ctx['location'] = location_id
2445 ctx['lot_id'] = lot_id
2446 ctx['owner_id'] = owner_id
2447 ctx['package_id'] = package_id
2448 obj_product = self.pool.get('product.product').browse(cr, uid, product, context=ctx)
2449 th_qty = obj_product.qty_available
2450 if uom and uom != obj_product.uom_id.id:
2451 uom_record = uom_obj.browse(cr, uid, uom, context=context)
2452 th_qty = uom_obj._compute_qty_obj(cr, uid, obj_product.uom_id, th_qty, uom_record)
2453 return {'value': {'th_qty': th_qty, 'product_uom_id': uom or obj_product.uom_id.id}}
2456 #----------------------------------------------------------
2458 #----------------------------------------------------------
2459 class stock_warehouse(osv.osv):
2460 _name = "stock.warehouse"
2461 _description = "Warehouse"
2464 'name': fields.char('Warehouse Name', size=128, required=True, select=True),
2465 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
2466 'partner_id': fields.many2one('res.partner', 'Address'),
2467 'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]),
2468 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage', '=', 'internal')]),
2469 'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"),
2470 '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'),
2471 'reception_steps': fields.selection([
2472 ('one_step', 'Receive goods directly in stock (1 step)'),
2473 ('two_steps', 'Unload in input location then go to stock (2 steps)'),
2474 ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments', required=True),
2475 'delivery_steps': fields.selection([
2476 ('ship_only', 'Ship directly from stock (Ship only)'),
2477 ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
2478 ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], 'Outgoing Shippings', required=True),
2479 'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'),
2480 'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'),
2481 'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'),
2482 'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'),
2483 'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'),
2484 'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'),
2485 'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'),
2486 'out_type_id': fields.many2one('stock.picking.type', 'Out Type'),
2487 'in_type_id': fields.many2one('stock.picking.type', 'In Type'),
2488 'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'),
2489 'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'),
2490 'reception_route_id': fields.many2one('stock.location.route', 'Reception Route'),
2491 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'),
2492 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'),
2493 'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'),
2494 'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes'),
2495 'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse'),
2498 def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None):
2499 resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))])
2500 if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id
2501 resupply_wh_ids.add(default_resupply_wh_id)
2502 resupply_wh_ids = list(resupply_wh_ids)
2503 return {'value': {'resupply_wh_ids': resupply_wh_ids}}
2505 def _get_inter_wh_location(self, cr, uid, warehouse, context=None):
2506 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2507 data_obj = self.pool.get('ir.model.data')
2509 inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1]
2511 inter_wh_loc = False
2514 def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None):
2515 return self.pool.get('product.product').search(cr, uid, [], context=context)
2517 def _assign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2518 product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2519 self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2521 def _unassign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2522 product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2523 self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(3, inter_wh_route_id)]}, context=context)
2525 def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2527 'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2528 'warehouse_selectable': False,
2529 'product_selectable': True,
2530 'product_categ_selectable': True,
2531 'supplied_wh_id': warehouse.id,
2532 'supplier_wh_id': wh.id,
2535 def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2536 location_obj = self.pool.get('stock.location')
2537 route_obj = self.pool.get('stock.location.route')
2538 pull_obj = self.pool.get('procurement.rule')
2539 #create route selectable on the product to resupply the warehouse from another one
2540 inter_wh_location_id = self._get_inter_wh_location(cr, uid, warehouse, context=context)
2541 if inter_wh_location_id:
2542 input_loc = warehouse.wh_input_stock_loc_id
2543 if warehouse.reception_steps == 'one_step':
2544 input_loc = warehouse.lot_stock_id
2545 inter_wh_location = location_obj.browse(cr, uid, inter_wh_location_id, context=context)
2546 for wh in supplier_warehouses:
2547 output_loc = wh.wh_output_stock_loc_id
2548 if wh.delivery_steps == 'ship_only':
2549 output_loc = wh.lot_stock_id
2550 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2551 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2552 values = [(output_loc, inter_wh_location, wh.out_type_id.id, wh), (inter_wh_location, input_loc, warehouse.in_type_id.id, warehouse)]
2553 pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context)
2554 for pull_rule in pull_rules_list:
2555 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2556 #if the warehouse is also set as default resupply method, assign this route automatically to all product
2557 if default_resupply_wh and default_resupply_wh.id == wh.id:
2558 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2559 #finally, save the route on the warehouse
2560 self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2562 def _default_stock_id(self, cr, uid, context=None):
2563 #lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock')
2565 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2566 return warehouse.lot_stock_id.id
2571 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2572 'lot_stock_id': _default_stock_id,
2573 'reception_steps': 'one_step',
2574 'delivery_steps': 'ship_only',
2576 _sql_constraints = [
2577 ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2578 ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2581 def _get_partner_locations(self, cr, uid, ids, context=None):
2582 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2583 data_obj = self.pool.get('ir.model.data')
2584 location_obj = self.pool.get('stock.location')
2586 customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2587 supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2589 customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2590 customer_loc = customer_loc and customer_loc[0] or False
2591 supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2592 supplier_loc = supplier_loc and supplier_loc[0] or False
2593 if not (customer_loc and supplier_loc):
2594 raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2595 return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2597 def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2598 location_obj = self.pool.get('stock.location')
2600 new_reception_step = new_reception_step or warehouse.reception_steps
2601 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2602 if warehouse.reception_steps != new_reception_step:
2603 location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2604 if new_reception_step != 'one_step':
2605 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2606 if new_reception_step == 'three_steps':
2607 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2609 if warehouse.delivery_steps != new_delivery_step:
2610 location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
2611 if new_delivery_step != 'ship_only':
2612 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
2613 if new_delivery_step == 'pick_pack_ship':
2614 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
2617 def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
2619 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2620 'product_categ_selectable': True,
2621 'product_selectable': False,
2625 def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None):
2626 pull_rules_list = []
2627 for from_loc, dest_loc, pick_type_id, warehouse in values:
2628 pull_rules_list.append({
2629 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2630 'location_src_id': from_loc.id,
2631 'location_id': dest_loc.id,
2632 'route_id': new_route_id,
2634 'picking_type_id': pick_type_id,
2635 'procure_method': 'make_to_order',
2636 'warehouse_id': supplied_warehouse.id,
2637 'propagate_warehouse_id': warehouse.id,
2639 return pull_rules_list
2641 def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
2643 push_rules_list = []
2644 pull_rules_list = []
2645 for from_loc, dest_loc, pick_type_id in values:
2646 push_rules_list.append({
2647 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2648 'location_from_id': from_loc.id,
2649 'location_dest_id': dest_loc.id,
2650 'route_id': new_route_id,
2652 'picking_type_id': pick_type_id,
2654 'warehouse_id': warehouse.id,
2656 pull_rules_list.append({
2657 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2658 'location_src_id': from_loc.id,
2659 'location_id': dest_loc.id,
2660 'route_id': new_route_id,
2662 'picking_type_id': pick_type_id,
2663 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
2665 'warehouse_id': warehouse.id,
2668 return push_rules_list, pull_rules_list
2670 def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
2671 route_obj = self.pool.get('stock.location.route')
2672 data_obj = self.pool.get('ir.model.data')
2674 mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
2676 mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('Make To Order'))], context=context)
2677 mto_route_id = mto_route_id and mto_route_id[0] or False
2678 if not mto_route_id:
2679 raise osv.except_osv(_('Error!'), _('Can\'t find any generic Make To Order route.'))
2681 from_loc, dest_loc, pick_type_id = values[0]
2683 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
2684 'location_src_id': from_loc.id,
2685 'location_id': dest_loc.id,
2686 'route_id': mto_route_id,
2688 'picking_type_id': pick_type_id,
2689 'procure_method': 'make_to_order',
2691 'warehouse_id': warehouse.id,
2694 def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
2696 'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2697 'warehouse_selectable': False,
2698 'product_selectable': True,
2699 'product_categ_selectable': True,
2700 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
2704 def create_routes(self, cr, uid, ids, warehouse, context=None):
2706 route_obj = self.pool.get('stock.location.route')
2707 pull_obj = self.pool.get('procurement.rule')
2708 push_obj = self.pool.get('stock.location.path')
2709 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2710 #create reception route and rules
2711 route_name, values = routes_dict[warehouse.reception_steps]
2712 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2713 reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
2714 wh_route_ids.append((4, reception_route_id))
2715 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
2716 #create the push/pull rules
2717 for push_rule in push_rules_list:
2718 push_obj.create(cr, uid, vals=push_rule, context=context)
2719 for pull_rule in pull_rules_list:
2720 #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
2721 pull_rule['procure_method'] = 'make_to_order'
2722 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2724 #create MTS route and pull rules for delivery and a specific route MTO to be set on the product
2725 route_name, values = routes_dict[warehouse.delivery_steps]
2726 route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2727 #create the route and its pull rules
2728 delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
2729 wh_route_ids.append((4, delivery_route_id))
2730 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
2731 for pull_rule in pull_rules_list:
2732 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2733 #create MTO pull rule and link it to the generic MTO route
2734 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2735 mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
2737 #create a route for cross dock operations, that can be set on products and product categories
2738 route_name, values = routes_dict['crossdock']
2739 crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
2740 crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
2741 wh_route_ids.append((4, crossdock_route_id))
2742 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)
2743 for pull_rule in pull_rules_list:
2744 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2746 #create route selectable on the product to resupply the warehouse from another one
2747 self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
2749 #return routes and mto pull rule to store on the warehouse
2751 'route_ids': wh_route_ids,
2752 'mto_pull_id': mto_pull_id,
2753 'reception_route_id': reception_route_id,
2754 'delivery_route_id': delivery_route_id,
2755 'crossdock_route_id': crossdock_route_id,
2758 def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2759 picking_type_obj = self.pool.get('stock.picking.type')
2760 pull_obj = self.pool.get('procurement.rule')
2761 push_obj = self.pool.get('stock.location.path')
2762 route_obj = self.pool.get('stock.location.route')
2763 new_reception_step = new_reception_step or warehouse.reception_steps
2764 new_delivery_step = new_delivery_step or warehouse.delivery_steps
2766 #change the default source and destination location and (de)activate picking types
2767 input_loc = warehouse.wh_input_stock_loc_id
2768 if new_reception_step == 'one_step':
2769 input_loc = warehouse.lot_stock_id
2770 output_loc = warehouse.wh_output_stock_loc_id
2771 if new_delivery_step == 'ship_only':
2772 output_loc = warehouse.lot_stock_id
2773 picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
2774 picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
2775 picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
2776 picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
2778 routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2779 #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
2780 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
2781 route_name, values = routes_dict[new_delivery_step]
2782 route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2783 dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
2784 #create the pull rules
2785 for pull_rule in pull_rules_list:
2786 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2788 #update reception route and rules: unlink the existing rules of the warehouse reception route and recreate it
2789 pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
2790 push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
2791 route_name, values = routes_dict[new_reception_step]
2792 route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2793 push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
2794 #create the push/pull rules
2795 for push_rule in push_rules_list:
2796 push_obj.create(cr, uid, vals=push_rule, context=context)
2797 for pull_rule in pull_rules_list:
2798 #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
2799 pull_rule['procure_method'] = 'make_to_order'
2800 pull_obj.create(cr, uid, vals=pull_rule, context=context)
2802 route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
2805 dummy, values = routes_dict[new_delivery_step]
2806 mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2807 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
2810 def create(self, cr, uid, vals, context=None):
2815 data_obj = self.pool.get('ir.model.data')
2816 seq_obj = self.pool.get('ir.sequence')
2817 picking_type_obj = self.pool.get('stock.picking.type')
2818 location_obj = self.pool.get('stock.location')
2820 #create view location for warehouse
2821 wh_loc_id = location_obj.create(cr, uid, {
2822 'name': _(vals.get('code')),
2824 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1]
2826 vals['view_location_id'] = wh_loc_id
2827 #create all location
2828 def_values = self.default_get(cr, uid, {'reception_steps', 'delivery_steps'})
2829 reception_steps = vals.get('reception_steps', def_values['reception_steps'])
2830 delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
2831 context_with_inactive = context.copy()
2832 context_with_inactive['active_test'] = False
2834 {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
2835 {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
2836 {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
2837 {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
2838 {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
2840 for values in sub_locations:
2841 location_id = location_obj.create(cr, uid, {
2842 'name': values['name'],
2843 'usage': 'internal',
2844 'location_id': wh_loc_id,
2845 'active': values['active'],
2846 }, context=context_with_inactive)
2847 vals[values['field']] = location_id
2849 #create new sequences
2850 in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence in'), 'prefix': vals.get('code', '') + '/IN/', 'padding': 5}, context=context)
2851 out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence out'), 'prefix': vals.get('code', '') + '/OUT/', 'padding': 5}, context=context)
2852 pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence packing'), 'prefix': vals.get('code', '') + '/PACK/', 'padding': 5}, context=context)
2853 pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence picking'), 'prefix': vals.get('code', '') + '/PICK/', 'padding': 5}, context=context)
2854 int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence internal'), 'prefix': vals.get('code', '') + '/INT/', 'padding': 5}, context=context)
2857 new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
2859 warehouse = self.browse(cr, uid, new_id, context=context)
2860 wh_stock_loc = warehouse.lot_stock_id
2861 wh_input_stock_loc = warehouse.wh_input_stock_loc_id
2862 wh_output_stock_loc = warehouse.wh_output_stock_loc_id
2863 wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
2865 #fetch customer and supplier locations, for references
2866 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context)
2868 #create in, out, internal picking types for warehouse
2869 input_loc = wh_input_stock_loc
2870 if warehouse.reception_steps == 'one_step':
2871 input_loc = wh_stock_loc
2872 output_loc = wh_output_stock_loc
2873 if warehouse.delivery_steps == 'ship_only':
2874 output_loc = wh_stock_loc
2876 #choose the next available color for the picking types of this warehouse
2878 available_colors = [c%9 for c in range(3, 12)] # put flashy colors first
2879 all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
2880 #don't use sets to preserve the list order
2881 for x in all_used_colors:
2882 if x['color'] in available_colors:
2883 available_colors.remove(x['color'])
2884 if available_colors:
2885 color = available_colors[0]
2887 #order the picking types with a sequence allowing to have the following suit for each warehouse: reception, internal, pick, pack, ship.
2888 max_sequence = self.pool.get('stock.picking.type').search_read(cr, uid, [], ['sequence'], order='sequence desc')
2889 max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
2891 in_type_id = picking_type_obj.create(cr, uid, vals={
2892 'name': _('Receptions'),
2893 'warehouse_id': new_id,
2895 'auto_force_assign': True,
2896 'sequence_id': in_seq_id,
2897 'default_location_src_id': supplier_loc.id,
2898 'default_location_dest_id': input_loc.id,
2899 'sequence': max_sequence + 1,
2900 'color': color}, context=context)
2901 out_type_id = picking_type_obj.create(cr, uid, vals={
2902 'name': _('Delivery Orders'),
2903 'warehouse_id': new_id,
2905 'sequence_id': out_seq_id,
2906 'return_picking_type_id': in_type_id,
2907 'default_location_src_id': output_loc.id,
2908 'default_location_dest_id': customer_loc.id,
2909 'sequence': max_sequence + 4,
2910 'color': color}, context=context)
2911 picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context)
2912 int_type_id = picking_type_obj.create(cr, uid, vals={
2913 'name': _('Internal Transfers'),
2914 'warehouse_id': new_id,
2916 'sequence_id': int_seq_id,
2917 'default_location_src_id': wh_stock_loc.id,
2918 'default_location_dest_id': wh_stock_loc.id,
2920 'sequence': max_sequence + 2,
2921 'color': color}, context=context)
2922 pack_type_id = picking_type_obj.create(cr, uid, vals={
2924 'warehouse_id': new_id,
2926 'sequence_id': pack_seq_id,
2927 'default_location_src_id': wh_pack_stock_loc.id,
2928 'default_location_dest_id': output_loc.id,
2929 'active': delivery_steps == 'pick_pack_ship',
2930 'sequence': max_sequence + 3,
2931 'color': color}, context=context)
2932 pick_type_id = picking_type_obj.create(cr, uid, vals={
2934 'warehouse_id': new_id,
2936 'sequence_id': pick_seq_id,
2937 'default_location_src_id': wh_stock_loc.id,
2938 'default_location_dest_id': wh_pack_stock_loc.id,
2939 'active': delivery_steps != 'ship_only',
2940 'sequence': max_sequence + 2,
2941 'color': color}, context=context)
2943 #write picking types on WH
2945 'in_type_id': in_type_id,
2946 'out_type_id': out_type_id,
2947 'pack_type_id': pack_type_id,
2948 'pick_type_id': pick_type_id,
2949 'int_type_id': int_type_id,
2951 super(stock_warehouse, self).write(cr, uid, new_id, vals=vals, context=context)
2954 #create routes and push/pull rules
2955 new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
2956 self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
2959 def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
2960 return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name
2962 def _format_routename(self, cr, uid, obj, name, context=None):
2963 return obj.name + ': ' + name
2965 def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
2966 #fetch customer and supplier locations, for references
2967 customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
2970 'one_step': (_('Reception in 1 step'), []),
2971 'two_steps': (_('Reception in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2972 'three_steps': (_('Reception in 3 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2973 '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)]),
2974 'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
2975 '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)]),
2976 '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)]),
2979 def _handle_renaming(self, cr, uid, warehouse, name, code, context=None):
2980 location_obj = self.pool.get('stock.location')
2981 route_obj = self.pool.get('stock.location.route')
2982 pull_obj = self.pool.get('procurement.rule')
2983 push_obj = self.pool.get('stock.location.path')
2985 location_id = warehouse.lot_stock_id.location_id.id
2986 location_obj.write(cr, uid, location_id, {'name': code}, context=context)
2987 #rename route and push-pull rules
2988 for route in warehouse.route_ids:
2989 route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context)
2990 for pull in route.pull_ids:
2991 pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2992 for push in route.push_ids:
2993 push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2994 #change the mto pull rule name
2995 pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
2997 def write(self, cr, uid, ids, vals, context=None):
3000 if isinstance(ids, (int, long)):
3002 seq_obj = self.pool.get('ir.sequence')
3003 route_obj = self.pool.get('stock.location.route')
3005 context_with_inactive = context.copy()
3006 context_with_inactive['active_test'] = False
3007 for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
3008 #first of all, check if we need to delete and recreate route
3009 if vals.get('reception_steps') or vals.get('delivery_steps'):
3010 #activate and deactivate location according to reception and delivery option
3011 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
3012 # switch between route
3013 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
3015 if vals.get('code') or vals.get('name'):
3016 name = warehouse.name
3018 if vals.get('name'):
3019 name = vals.get('name', warehouse.name)
3020 self._handle_renaming(cr, uid, warehouse, name, vals.get('code', warehouse.code), context=context_with_inactive)
3021 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)
3022 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)
3023 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)
3024 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)
3025 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)
3026 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
3027 for cmd in vals.get('resupply_wh_ids'):
3029 new_ids = set(cmd[2])
3030 old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
3031 to_add_wh_ids = new_ids - old_ids
3033 supplier_warehouses = self.browse(cr, uid, list(to_add_wh_ids), context=context)
3034 self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
3035 to_remove_wh_ids = old_ids - new_ids
3036 if to_remove_wh_ids:
3037 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)
3038 if to_remove_route_ids:
3039 route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
3043 if 'default_resupply_wh_id' in vals:
3044 if vals.get('default_resupply_wh_id') == warehouse.id:
3045 raise osv.except_osv(_('Warning'),_('The default resupply warehouse should be different than the warehouse itself!'))
3046 if warehouse.default_resupply_wh_id:
3047 #remove the existing resupplying route on all products
3048 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)
3049 for inter_wh_route_id in to_remove_route_ids:
3050 self._unassign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
3051 if vals.get('default_resupply_wh_id'):
3052 #assign the new resupplying route on all products
3053 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)
3054 for inter_wh_route_id in to_assign_route_ids:
3055 self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
3057 return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
3059 def unlink(self, cr, uid, ids, context=None):
3060 #TODO try to delete location and route and if not possible, put them in inactive
3061 return super(stock_warehouse, self).unlink(cr, uid, ids, context=context)
3063 def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
3064 all_routes = [route.id for route in warehouse.route_ids]
3065 all_routes += [warehouse.mto_pull_id.route_id.id]
3068 def view_all_routes_for_wh(self, cr, uid, ids, context=None):
3070 for wh in self.browse(cr, uid, ids, context=context):
3071 all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
3073 domain = [('id', 'in', all_routes)]
3075 'name': _('Warehouse\'s Routes'),
3077 'res_model': 'stock.location.route',
3078 'type': 'ir.actions.act_window',
3080 'view_mode': 'tree,form',
3081 'view_type': 'form',
3085 class stock_location_path(osv.osv):
3086 _name = "stock.location.path"
3087 _description = "Pushed Flows"
3090 def _get_route(self, cr, uid, ids, context=None):
3091 #WARNING TODO route_id is not required, so a field related seems a bad idea >-<
3097 context_with_inactive = context.copy()
3098 context_with_inactive['active_test'] = False
3099 for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive):
3100 for push_rule in route.push_ids:
3101 result[push_rule.id] = True
3102 return result.keys()
3104 def _get_rules(self, cr, uid, ids, context=None):
3106 for route in self.browse(cr, uid, ids, context=context):
3107 res += [x.id for x in route.push_ids]
3111 'name': fields.char('Operation Name', size=64, required=True),
3112 'company_id': fields.many2one('res.company', 'Company'),
3113 'route_id': fields.many2one('stock.location.route', 'Route'),
3114 'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
3115 'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
3116 'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
3117 'invoice_state': fields.selection([
3118 ("invoiced", "Invoiced"),
3119 ("2binvoiced", "To Be Invoiced"),
3120 ("none", "Not Applicable")], "Invoice Status",
3122 '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"),
3123 'auto': fields.selection(
3124 [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
3126 required=True, select=1,
3127 help="This is used to define paths the product has to follow within the location tree.\n" \
3128 "The 'Automatic Move' value will create a stock move after the current one that will be "\
3129 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
3130 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
3132 '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'),
3133 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={
3134 'stock.location.route': (_get_route, ['active'], 20),
3135 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),},
3136 help="If the active field is set to False, it will allow you to hide the rule without removing it." ),
3137 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
3138 'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence',
3140 'stock.location.route': (_get_rules, ['sequence'], 10),
3141 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10),
3143 'sequence': fields.integer('Sequence'),
3148 'invoice_state': 'none',
3149 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
3154 def _apply(self, cr, uid, rule, move, context=None):
3155 move_obj = self.pool.get('stock.move')
3156 newdate = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta.relativedelta(days=rule.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
3157 if rule.auto == 'transparent':
3158 old_dest_location = move.location_dest_id.id
3159 move_obj.write(cr, uid, [move.id], {
3161 'date_expected': newdate,
3162 'location_dest_id': rule.location_dest_id.id
3165 #avoid looping if a push rule is not well configured
3166 if rule.location_dest_id.id != old_dest_location:
3167 #call again push_apply to see if a next step is defined
3168 move_obj._push_apply(cr, uid, [move], context=context)
3170 move_id = move_obj.copy(cr, uid, move.id, {
3171 'location_id': move.location_dest_id.id,
3172 'location_dest_id': rule.location_dest_id.id,
3174 'company_id': rule.company_id and rule.company_id.id or False,
3175 'date_expected': newdate,
3176 'picking_id': False,
3177 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
3178 'propagate': rule.propagate,
3179 'push_rule_id': rule.id,
3180 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
3182 move_obj.write(cr, uid, [move.id], {
3183 'move_dest_id': move_id,
3185 move_obj.action_confirm(cr, uid, [move_id], context=None)
3187 class stock_move_putaway(osv.osv):
3188 _name = 'stock.move.putaway'
3189 _description = 'Proposed Destination'
3191 'move_id': fields.many2one('stock.move', required=True),
3192 'location_id': fields.many2one('stock.location', 'Location', required=True),
3193 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
3194 'quantity': fields.float('Quantity', required=True),
3199 # -------------------------
3200 # Packaging related stuff
3201 # -------------------------
3203 from openerp.report import report_sxw
3204 report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package', 'addons/stock/report/package_barcode.rml')
3206 class stock_package(osv.osv):
3208 These are the packages, containing quants and/or other packages
3210 _name = "stock.quant.package"
3211 _description = "Physical Packages"
3212 _parent_name = "parent_id"
3213 _parent_store = True
3214 _parent_order = 'name'
3215 _order = 'parent_left'
3217 def name_get(self, cr, uid, ids, context=None):
3218 res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
3221 def _complete_name(self, cr, uid, ids, name, args, context=None):
3222 """ Forms complete name of location from parent location to child location.
3223 @return: Dictionary of values
3226 for m in self.browse(cr, uid, ids, context=context):
3228 parent = m.parent_id
3230 res[m.id] = parent.name + ' / ' + res[m.id]
3231 parent = parent.parent_id
3234 def _get_packages(self, cr, uid, ids, context=None):
3235 """Returns packages from quants for store"""
3237 for quant in self.browse(cr, uid, ids, context=context):
3238 if quant.package_id:
3239 res.add(quant.package_id.id)
3242 def _get_packages_to_relocate(self, cr, uid, ids, context=None):
3244 for pack in self.browse(cr, uid, ids, context=context):
3247 res.add(pack.parent_id.id)
3250 # TODO: Problem when package is empty!
3252 def _get_package_info(self, cr, uid, ids, name, args, context=None):
3253 default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
3254 res = {}.fromkeys(ids, {'location_id': False, 'company_id': default_company_id})
3255 for pack in self.browse(cr, uid, ids, context=context):
3257 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
3258 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
3259 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
3260 elif pack.children_ids:
3261 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
3262 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
3263 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
3267 'name': fields.char('Package Reference', size=64, select=True),
3268 'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
3269 'parent_left': fields.integer('Left Parent', select=1),
3270 'parent_right': fields.integer('Right Parent', select=1),
3271 'packaging_id': fields.many2one('product.packaging', 'Type of Packaging'),
3272 'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
3274 'stock.quant': (_get_packages, ['location_id'], 10),
3275 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3277 'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content', readonly=True),
3278 'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict', readonly=True),
3279 'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages', readonly=True),
3280 'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package",
3282 'stock.quant': (_get_packages, ['company_id'], 10),
3283 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3285 'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
3287 'stock.quant': (_get_packages, ['owner_id'], 10),
3288 'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3292 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3295 def _check_location_constraint(self, cr, uid, ids, context=None):
3296 '''checks that all quants in a package are stored in the same location. This function cannot be used
3297 as a constraint because it needs to be checked on pack operations (they may not call write on the
3300 quant_obj = self.pool.get('stock.quant')
3301 for pack in self.browse(cr, uid, ids, context=context):
3303 while parent.parent_id:
3304 parent = parent.parent_id
3305 quant_ids = self.get_content(cr, uid, [parent.id], context=context)
3306 quants = quant_obj.browse(cr, uid, quant_ids, context=context)
3307 location_id = quants and quants[0].location_id.id or False
3308 if not all([quant.location_id.id == location_id for quant in quants if quant.qty > 0]):
3309 raise osv.except_osv(_('Error'), _('Everything inside a package should be in the same location'))
3312 def action_print(self, cr, uid, ids, context=None):
3316 'ids': context.get('active_id') and [context.get('active_id')] or ids,
3317 'model': 'stock.quant.package',
3318 'form': self.read(cr, uid, ids)[0]
3321 'type': 'ir.actions.report.xml',
3322 'report_name': 'stock.quant.package.barcode',
3326 def unpack(self, cr, uid, ids, context=None):
3327 quant_obj = self.pool.get('stock.quant')
3328 for package in self.browse(cr, uid, ids, context=context):
3329 quant_ids = [quant.id for quant in package.quant_ids]
3330 quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3331 children_package_ids = [child_package.id for child_package in package.children_ids]
3332 self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3333 #delete current package since it contains nothing anymore
3334 self.unlink(cr, uid, ids, context=context)
3335 return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3337 def get_content(self, cr, uid, ids, context=None):
3338 child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3339 return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3341 def get_content_package(self, cr, uid, ids, context=None):
3342 quants_ids = self.get_content(cr, uid, ids, context=context)
3343 res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3344 res['domain'] = [('id', 'in', quants_ids)]
3347 def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3348 ''' find the total of given product 'product_id' inside the given package 'package_id'''
3349 quant_obj = self.pool.get('stock.quant')
3350 all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3352 for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3353 if quant.product_id.id == product_id:
3357 def _get_all_products_quantities(self, cr, uid, package_id, context=None):
3358 '''This function computes the different product quantities for the given package
3360 quant_obj = self.pool.get('stock.quant')
3362 for quant in quant_obj.browse(cr, uid, self.get_content(cr, uid, package_id, context=context)):
3363 if quant.product_id.id not in res:
3364 res[quant.product_id.id] = 0
3365 res[quant.product_id.id] += quant.qty
3368 def copy(self, cr, uid, id, default=None, context=None):
3371 if not default.get('name'):
3372 default['name'] = self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3373 return super(stock_package, self).copy(cr, uid, id, default, context=context)
3375 def copy_pack(self, cr, uid, id, default_pack_values=None, default=None, context=None):
3376 stock_pack_operation_obj = self.pool.get('stock.pack.operation')
3379 new_package_id = self.copy(cr, uid, id, default_pack_values, context=context)
3380 default['result_package_id'] = new_package_id
3381 op_ids = stock_pack_operation_obj.search(cr, uid, [('result_package_id', '=', id)], context=context)
3382 for op_id in op_ids:
3383 stock_pack_operation_obj.copy(cr, uid, op_id, default, context=context)
3386 class stock_pack_operation(osv.osv):
3387 _name = "stock.pack.operation"
3388 _description = "Packing Operation"
3390 def _get_remaining_prod_quantities(self, cr, uid, operation, context=None):
3391 '''Get the remaining quantities per product on an operation with a package. This function returns a dictionary'''
3392 #if the operation doesn't concern a package, it's not relevant to call this function
3393 if not operation.package_id or operation.product_id:
3394 return {operation.product_id.id: operation.remaining_qty}
3395 #get the total of products the package contains
3396 res = self.pool.get('stock.quant.package')._get_all_products_quantities(cr, uid, operation.package_id.id, context=context)
3397 #reduce by the quantities linked to a move
3398 for record in operation.linked_move_operation_ids:
3399 if record.move_id.product_id.id not in res:
3400 res[record.move_id.product_id.id] = 0
3401 res[record.move_id.product_id.id] -= record.qty
3404 def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3405 uom_obj = self.pool.get('product.uom')
3407 for ops in self.browse(cr, uid, ids, context=context):
3409 if ops.package_id and not ops.product_id:
3410 #dont try to compute the remaining quantity for packages because it's not relevant (a package could include different products).
3411 #should use _get_remaining_prod_quantities instead
3414 qty = ops.product_qty
3415 if ops.product_uom_id:
3416 qty = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, ops.product_qty, ops.product_id.uom_id.id)
3417 for record in ops.linked_move_operation_ids:
3419 #converting the remaining quantity in the pack operation UoM
3420 if ops.product_uom_id:
3421 qty = uom_obj._compute_qty(cr, uid, ops.product_id.uom_id.id, qty, ops.product_uom_id.id)
3425 def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3426 res = self.on_change_tests(cr, uid, ids, product_id, product_uom_id, product_qty, context=context)
3427 if product_id and not product_uom_id:
3428 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3429 res['value']['product_uom_id'] = product.uom_id.id
3432 def on_change_tests(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3434 uom_obj = self.pool.get('product.uom')
3436 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3437 product_uom_id = product_uom_id or product.uom_id.id
3438 selected_uom = uom_obj.browse(cr, uid, product_uom_id, context=context)
3439 if selected_uom.category_id.id != product.uom_id.category_id.id:
3441 'title': _('Warning: wrong UoM!'),
3442 '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)
3444 if product_qty and 'warning' not in res:
3445 rounded_qty = uom_obj._compute_qty(cr, uid, product_uom_id, product_qty, product_uom_id, round=True)
3446 if rounded_qty != product_qty:
3448 'title': _('Warning: wrong quantity!'),
3449 'message': _('The chosen quantity for product %s is not compatible with the UoM rounding. It will be automatically converted at confirmation') % (product.name)
3454 'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3455 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
3456 'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3457 'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3458 'package_id': fields.many2one('stock.quant.package', 'Package'), # 2
3459 'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3460 'result_package_id': fields.many2one('stock.quant.package', 'Container Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3461 'date': fields.datetime('Date', required=True),
3462 'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3463 #'update_cost': fields.boolean('Need cost update'),
3464 'cost': fields.float("Cost", help="Unit Cost for this product line"),
3465 'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3466 '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'),
3467 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'),
3471 'date': fields.date.context_today,
3474 def write(self, cr, uid, ids, vals, context=None):
3475 res = super(stock_pack_operation, self).write(cr, uid, ids, vals, context=context)
3476 if isinstance(ids, (int, long)):
3478 self.recompute_rem_qty_from_operation(cr, uid, ids, context=context)
3481 def create(self, cr, uid, vals, context=None):
3482 res_id = super(stock_pack_operation, self).create(cr, uid, vals, context=context)
3483 self.recompute_rem_qty_from_operation(cr, uid, [res_id], context=context)
3486 def recompute_rem_qty_from_operation(self, cr, uid, op_ids, context=None):
3487 def _create_link_for_product(product_id, qty):
3489 for move in sorted_moves:
3490 if move.product_id.id == product_id and move.state not in ['done', 'cancel']:
3491 qty_on_link = min(move.remaining_qty, qty_to_assign)
3492 link_obj.create(cr, uid, {'move_id': move.id, 'operation_id': op.id, 'qty': qty_on_link}, context=context)
3493 qty_to_assign -= qty_on_link
3495 if qty_to_assign <= 0:
3498 def _check_quants_reserved(ops):
3499 if ops.package_id and not ops.product_id:
3500 for quant in quant_obj.browse(cr, uid, package_obj.get_content(cr, uid, [ops.package_id.id]), context=context):
3501 if quant.reservation_id and quant.reservation_id.id in [x.id for x in ops.picking_id.move_lines] and (not quants_done.get(quant.id)):
3502 #Entire packages means entire quants from those packages
3503 if not quants_done.get(quant.id):
3504 quants_done[quant.id] = 0
3505 link_obj.create(cr, uid, {'move_id': quant.reservation_id.id, 'operation_id': ops.id, 'qty': quant.qty}, context=context)
3507 qty = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, ops.product_qty, ops.product_id.uom_id.id)
3508 #Check moves with same product
3509 for move in [x for x in ops.picking_id.move_lines if ops.product_id.id == x.product_id.id]:
3510 for quant in move.reserved_quant_ids:
3514 flag = quant.package_id and bool(package_obj.search(cr, uid, [('id', 'child_of', [ops.package_id.id]), ('id', '=', quant.package_id.id)], context=context)) or False
3516 flag = not quant.package_id.id
3517 flag = flag and ((ops.lot_id and ops.lot_id.id == quant.lot_id.id) or not ops.lot_id)
3518 flag = flag and (ops.owner_id.id == quant.owner_id.id)
3520 quant_qty = quant.qty
3521 if quants_done.get(quant.id):
3522 if quants_done[quant.id] == 0:
3524 quant_qty = quants_done[quant.id]
3527 quants_done[quant.id] = quant_qty - qty
3529 qty_todo = quant_qty
3530 quants_done[quant.id] = 0
3532 link_obj.create(cr, uid, {'move_id': quant.reservation_id.id, 'operation_id': ops.id, 'qty': qty_todo}, context=context)
3534 link_obj = self.pool.get('stock.move.operation.link')
3535 uom_obj = self.pool.get('product.uom')
3536 package_obj = self.pool.get('stock.quant.package')
3537 quant_obj = self.pool.get('stock.quant')
3540 operations = self.browse(cr, uid, op_ids, context=context)
3541 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))
3543 for op in operations:
3544 if not sorted_moves:
3545 #sort moves in order to process first the ones that have already reserved quants
3546 sorted_moves = op.picking_id.move_lines
3547 sorted_moves.sort(key=lambda x: x.product_qty - x.reserved_availability)
3549 to_unlink_ids = [x.id for x in op.linked_move_operation_ids]
3551 link_obj.unlink(cr, uid, to_unlink_ids, context=context)
3552 _check_quants_reserved(op)
3554 for op in operations:
3557 #TODO: Remaining qty: UoM conversions are done twice
3558 normalized_qty = uom_obj._compute_qty(cr, uid, op.product_uom_id.id, op.remaining_qty, op.product_id.uom_id.id)
3559 if normalized_qty > 0:
3560 _create_link_for_product(op.product_id.id, normalized_qty)
3562 prod_quants = self._get_remaining_prod_quantities(cr, uid, op, context=context)
3563 for product_id, qty in prod_quants.items():
3565 _create_link_for_product(product_id, qty)
3567 def process_packaging(self, cr, uid, operation, quants, context=None):
3568 ''' Process the packaging of a given operation, after the quants have been moved. If there was not enough quants found
3569 a quant already has been with the good package information so we don't consider that case in this method'''
3570 quant_obj = self.pool.get("stock.quant")
3571 pack_obj = self.pool.get("stock.quant.package")
3572 for quant in quants:
3574 if operation.product_id:
3575 #if a product + a package information is given, we consider that we took a part of an existing package (unpacking)
3576 quant_obj.write(cr, SUPERUSER_ID, quant, {'package_id': operation.result_package_id.id}, context=context)
3577 elif operation.package_id and operation.result_package_id:
3578 #move the whole pack into the final package if any
3579 pack_obj.write(cr, uid, [operation.package_id.id], {'parent_id': operation.result_package_id.id}, context=context)
3584 #TODO: this function can be refactored
3585 def _search_and_increment(self, cr, uid, picking_id, domain, context=None):
3586 '''Search for an operation with given 'domain' in a picking, if it exists increment the qty (+1) otherwise create it
3588 :param domain: list of tuple directly reusable as a domain
3589 context can receive a key 'current_package_id' with the package to consider for this operation
3592 previously: returns the update to do in stock.move one2many field of picking (adapt remaining quantities) and to the list of package in the classic one2many syntax
3593 (0, 0, { values }) link to a new record that needs to be created with the given values dictionary
3594 (1, ID, { values }) update the linked record with id = ID (write *values* on it)
3595 (2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
3600 #if current_package_id is given in the context, we increase the number of items in this package
3601 package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3602 existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id)] + domain + package_clause, context=context)
3603 if existing_operation_ids:
3604 #existing operation found for the given domain and picking => increment its quantity
3605 operation_id = existing_operation_ids[0]
3606 qty = self.browse(cr, uid, operation_id, context=context).product_qty + 1
3607 self.write(cr, uid, [operation_id], {'product_qty': qty}, context=context)
3609 #no existing operation found for the given domain and picking => create a new one
3611 'picking_id': picking_id,
3615 var_name, dummy, value = key
3617 if var_name == 'product_id':
3618 uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
3619 update_dict = {var_name: value}
3621 update_dict['product_uom_id'] = uom_id
3622 values.update(update_dict)
3623 operation_id = self.create(cr, uid, values, context=context)
3627 class stock_move_operation_link(osv.osv):
3629 Table making the link between stock.moves and stock.pack.operations to compute the remaining quantities on each of these objects
3631 _name = "stock.move.operation.link"
3632 _description = "Link between stock moves and pack operations"
3635 '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."),
3636 'operation_id': fields.many2one('stock.pack.operation', 'Operation', required=True, ondelete="cascade"),
3637 'move_id': fields.many2one('stock.move', 'Move', required=True, ondelete="cascade"),
3638 'reserved_quant_ids': fields.one2many('stock.quant', 'link_move_operation_id', 'Reserved quants'),
3641 def get_specific_domain(self, cr, uid, record, context=None):
3642 '''Returns the specific domain to consider for quant selection in action_assign() or action_done() of stock.move,
3643 having the record given as parameter making the link between the stock move and a pack operation'''
3645 op = record.operation_id
3647 if op.package_id and op.product_id:
3648 #if removing a product from a box, we restrict the choice of quants to this box
3649 domain.append(('package_id', '=', op.package_id.id))
3651 #if moving a box, we allow to take everything from inside boxes as well
3652 domain.append(('package_id', 'child_of', [op.package_id.id]))
3654 #if not given any information about package, we don't open boxes
3655 domain.append(('package_id', '=', False))
3656 #if lot info is given, we restrict choice to this lot otherwise we can take any
3658 domain.append(('lot_id', '=', op.lot_id.id))
3659 #if owner info is given, we restrict to this owner otherwise we restrict to no owner
3661 domain.append(('owner_id', '=', op.owner_id.id))
3663 domain.append(('owner_id', '=', False))
3666 class stock_warehouse_orderpoint(osv.osv):
3668 Defines Minimum stock rules.
3670 _name = "stock.warehouse.orderpoint"
3671 _description = "Minimum Inventory Rule"
3673 def get_draft_procurements(self, cr, uid, ids, context=None):
3676 if not isinstance(ids, list):
3678 procurement_obj = self.pool.get('procurement.order')
3679 for orderpoint in self.browse(cr, uid, ids, context=context):
3680 procurement_ids = procurement_obj.search(cr, uid, [('state', 'not in', ('cancel', 'done')), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)], context=context)
3681 return list(set(procurement_ids))
3683 def _check_product_uom(self, cr, uid, ids, context=None):
3685 Check if the UoM has the same category as the product standard UoM
3690 for rule in self.browse(cr, uid, ids, context=context):
3691 if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
3696 def action_view_proc_to_process(self, cr, uid, ids, context=None):
3697 act_obj = self.pool.get('ir.actions.act_window')
3698 mod_obj = self.pool.get('ir.model.data')
3699 draft_ids = self.get_draft_procurements(cr, uid, ids, context=context)
3700 result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
3704 result = act_obj.read(cr, uid, [result[1]], context=context)[0]
3705 result['domain'] = "[('id', 'in', [" + ','.join(map(str, draft_ids)) + "])]"
3709 'name': fields.char('Name', size=32, required=True),
3710 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
3711 'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
3712 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
3713 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
3714 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '!=', 'service')]),
3715 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
3716 'product_min_qty': fields.float('Minimum Quantity', required=True,
3717 help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
3718 "a procurement to bring the forecasted quantity to the Max Quantity."),
3719 'product_max_qty': fields.float('Maximum Quantity', required=True,
3720 help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
3721 "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
3722 'qty_multiple': fields.integer('Qty Multiple', required=True,
3723 help="The procurement quantity will be rounded up to this multiple."),
3724 'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
3725 'company_id': fields.many2one('res.company', 'Company', required=True)
3728 'active': lambda *a: 1,
3729 'logic': lambda *a: 'max',
3730 'qty_multiple': lambda *a: 1,
3731 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3732 'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
3733 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
3735 _sql_constraints = [
3736 ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
3739 (_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']),
3742 def default_get(self, cr, uid, fields, context=None):
3743 res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
3744 # default 'warehouse_id' and 'location_id'
3745 if 'warehouse_id' not in res:
3746 warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0', context)
3747 res['warehouse_id'] = warehouse.id
3748 if 'location_id' not in res:
3749 warehouse = self.pool.get('stock.warehouse').browse(cr, uid, res['warehouse_id'], context)
3750 res['location_id'] = warehouse.lot_stock_id.id
3753 def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
3754 """ Finds location id for changed warehouse.
3755 @param warehouse_id: Changed id of warehouse.
3756 @return: Dictionary of values.
3759 w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
3760 v = {'location_id': w.lot_stock_id.id}
3764 def onchange_product_id(self, cr, uid, ids, product_id, context=None):
3765 """ Finds UoM for changed product.
3766 @param product_id: Changed id of product.
3767 @return: Dictionary of values.
3770 prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3771 d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
3772 v = {'product_uom': prod.uom_id.id}
3773 return {'value': v, 'domain': d}
3774 return {'domain': {'product_uom': []}}
3776 def copy(self, cr, uid, id, default=None, context=None):
3780 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3782 return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
3785 class stock_picking_type(osv.osv):
3786 _name = "stock.picking.type"
3787 _description = "The picking type determines the picking view"
3790 def _get_tristate_values(self, cr, uid, ids, field_name, arg, context=None):
3791 picking_obj = self.pool.get('stock.picking')
3792 res = dict.fromkeys(ids, [])
3793 for picking_type_id in ids:
3794 #get last 10 pickings of this type
3795 picking_ids = picking_obj.search(cr, uid, [('picking_type_id', '=', picking_type_id), ('state', '=', 'done')], order='date_done desc', limit=10, context=context)
3797 for picking in picking_obj.browse(cr, uid, picking_ids, context=context):
3798 if picking.date_done > picking.date:
3799 tristates.insert(0, {'tooltip': picking.name + _(': Late'), 'value': -1})
3800 elif picking.backorder_id:
3801 tristates.insert(0, {'tooltip': picking.name + _(': Backorder exists'), 'value': 0})
3803 tristates.insert(0, {'tooltip': picking.name + _(': OK'), 'value': 1})
3804 res[picking_type_id] = tristates
3807 def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
3808 obj = self.pool.get('stock.picking')
3810 'count_picking_draft': [('state', '=', 'draft')],
3811 'count_picking_waiting': [('state', '=', 'confirmed')],
3812 'count_picking_ready': [('state', 'in', ('assigned', 'partially_available'))],
3813 'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
3814 'count_picking_late': [('min_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
3815 'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting', 'partially_available'))],
3818 for field in domains:
3819 data = obj.read_group(cr, uid, domains[field] +
3820 [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', ids)],
3821 ['picking_type_id'], ['picking_type_id'], context=context)
3822 count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
3824 result.setdefault(tid, {})[field] = count.get(tid, 0)
3826 if result[tid]['count_picking']:
3827 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] * 100 / result[tid]['count_picking']
3828 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] * 100 / result[tid]['count_picking']
3830 result[tid]['rate_picking_late'] = 0
3831 result[tid]['rate_picking_backorders'] = 0
3834 #TODO: not returning valus in required format to show in sparkline library,just added latest_picking_waiting need to add proper logic.
3835 def _get_picking_history(self, cr, uid, ids, field_names, arg, context=None):
3836 obj = self.pool.get('stock.picking')
3840 'latest_picking_late': [],
3841 'latest_picking_backorders': [],
3842 'latest_picking_waiting': []
3845 pick_ids = obj.search(cr, uid, [('state', '=','done'), ('picking_type_id','=',type_id)], limit=12, order="date desc", context=context)
3846 for pick in obj.browse(cr, uid, pick_ids, context=context):
3847 result[type_id]['latest_picking_late'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3848 result[type_id]['latest_picking_backorders'] = bool(pick.backorder_id)
3849 result[type_id]['latest_picking_waiting'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3852 def onchange_picking_code(self, cr, uid, ids, picking_code=False):
3853 if not picking_code:
3856 obj_data = self.pool.get('ir.model.data')
3857 stock_loc = obj_data.get_object_reference(cr, uid, 'stock','stock_location_stock')[1]
3860 'default_location_src_id': stock_loc,
3861 'default_location_dest_id': stock_loc,
3863 if picking_code == 'incoming':
3864 result['default_location_src_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_suppliers')[1]
3865 return {'value': result}
3866 if picking_code == 'outgoing':
3867 result['default_location_dest_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_customers')[1]
3868 return {'value': result}
3870 return {'value': result}
3872 def _get_name(self, cr, uid, ids, field_names, arg, context=None):
3873 return dict(self.name_get(cr, uid, ids, context=context))
3875 def name_get(self, cr, uid, ids, context=None):
3876 """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
3879 if not isinstance(ids, list):
3884 for record in self.browse(cr, uid, ids, context=context):
3886 if record.warehouse_id:
3887 name = record.warehouse_id.name + ': ' +name
3888 if context.get('special_shortened_wh_name'):
3889 if record.warehouse_id:
3890 name = record.warehouse_id.name
3892 name = _('Customer') + ' (' + record.name + ')'
3893 res.append((record.id, name))
3896 def _default_warehouse(self, cr, uid, context=None):
3897 user = self.pool.get('res.users').browse(cr, uid, uid, context)
3898 res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
3899 return res and res[0] or False
3902 'name': fields.char('Picking Type Name', translate=True, required=True),
3903 'complete_name': fields.function(_get_name, type='char', string='Name'),
3904 'auto_force_assign': fields.boolean('Automatic Availability', help='This picking type does\'t need to check for the availability in source location.'),
3905 'color': fields.integer('Color'),
3906 'sequence': fields.integer('Sequence', help="Used to order the 'All Operations' kanban view"),
3907 'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
3908 'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
3909 'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
3910 'code': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True),
3911 'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
3912 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
3913 'active': fields.boolean('Active'),
3915 # Statistics for the kanban view
3916 'last_done_picking': fields.function(_get_tristate_values,
3918 string='Last 10 Done Pickings'),
3920 'count_picking_draft': fields.function(_get_picking_count,
3921 type='integer', multi='_get_picking_count'),
3922 'count_picking_ready': fields.function(_get_picking_count,
3923 type='integer', multi='_get_picking_count'),
3924 'count_picking': fields.function(_get_picking_count,
3925 type='integer', multi='_get_picking_count'),
3926 'count_picking_waiting': fields.function(_get_picking_count,
3927 type='integer', multi='_get_picking_count'),
3928 'count_picking_late': fields.function(_get_picking_count,
3929 type='integer', multi='_get_picking_count'),
3930 'count_picking_backorders': fields.function(_get_picking_count,
3931 type='integer', multi='_get_picking_count'),
3933 'rate_picking_late': fields.function(_get_picking_count,
3934 type='integer', multi='_get_picking_count'),
3935 'rate_picking_backorders': fields.function(_get_picking_count,
3936 type='integer', multi='_get_picking_count'),
3938 'latest_picking_late': fields.function(_get_picking_history,
3939 type='string', multi='_get_picking_history'),
3940 'latest_picking_backorders': fields.function(_get_picking_history,
3941 type='string', multi='_get_picking_history'),
3942 'latest_picking_waiting': fields.function(_get_picking_history,
3943 type='string', multi='_get_picking_history'),
3947 'warehouse_id': _default_warehouse,
3952 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: