""" Cancels the moves and if all moves are cancelled it cancels the picking.
@return: True
"""
- if not len(ids):
- return True
- if context is None:
- context = {}
- pickings = set()
+ procurement_obj = self.pool.get('procurement.order')
+ context = context or {}
for move in self.browse(cr, uid, ids, context=context):
- if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
- if move.picking_id:
- pickings.add(move.picking_id.id)
- if move.move_dest_id and move.move_dest_id.state == 'waiting':
- self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context)
- if context.get('call_unlink',False) and move.move_dest_id.picking_id:
- workflow.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
- self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
- if not context.get('call_unlink',False):
- for pick in self.pool.get('stock.picking').browse(cr, uid, list(pickings), context=context):
- if all(move.state == 'cancel' for move in pick.move_lines):
- self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'}, context=context)
-
- for id in ids:
- workflow.trg_trigger(uid, 'stock.move', id, cr)
- return True
-
- def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
- """
- Return the accounts and journal to use to post Journal Entries for the real-time
- valuation of the move.
-
- :param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key
- :raise: osv.except_osv() is any mandatory account or journal is not defined.
- """
- product_obj=self.pool.get('product.product')
- accounts = product_obj.get_product_accounts(cr, uid, move.product_id.id, context)
- if move.location_id.valuation_out_account_id:
- acc_src = move.location_id.valuation_out_account_id.id
- else:
- acc_src = accounts['stock_account_input']
-
- if move.location_dest_id.valuation_in_account_id:
- acc_dest = move.location_dest_id.valuation_in_account_id.id
- else:
- acc_dest = accounts['stock_account_output']
-
- acc_valuation = accounts.get('property_stock_valuation_account_id', False)
- journal_id = accounts['stock_journal']
-
- if acc_dest == acc_valuation:
- raise osv.except_osv(_('Error!'), _('Cannot create Journal Entry, Output Account of this product and Valuation account on category of this product are same.'))
-
- if acc_src == acc_valuation:
- raise osv.except_osv(_('Error!'), _('Cannot create Journal Entry, Input Account of this product and Valuation account on category of this product are same.'))
-
- if not acc_src:
- raise osv.except_osv(_('Error!'), _('Please define stock input account for this product or its category: "%s" (id: %d)') % \
- (move.product_id.name, move.product_id.id,))
- if not acc_dest:
- raise osv.except_osv(_('Error!'), _('Please define stock output account for this product or its category: "%s" (id: %d)') % \
- (move.product_id.name, move.product_id.id,))
- if not journal_id:
- raise osv.except_osv(_('Error!'), _('Please define journal on the product category: "%s" (id: %d)') % \
- (move.product_id.categ_id.name, move.product_id.categ_id.id,))
- if not acc_valuation:
- raise osv.except_osv(_('Error!'), _('Please define inventory valuation account on the product category: "%s" (id: %d)') % \
- (move.product_id.categ_id.name, move.product_id.categ_id.id,))
- return journal_id, acc_src, acc_dest, acc_valuation
-
- def _get_reference_accounting_values_for_valuation(self, cr, uid, move, context=None):
- """
- Return the reference amount and reference currency representing the inventory valuation for this move.
- These reference values should possibly be converted before being posted in Journals to adapt to the primary
- and secondary currencies of the relevant accounts.
- """
- product_uom_obj = self.pool.get('product.uom')
-
- # by default the reference currency is that of the move's company
- reference_currency_id = move.company_id.currency_id.id
-
- default_uom = move.product_id.uom_id.id
- qty = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
-
- # if product is set to average price and a specific value was entered in the picking wizard,
- # we use it
- if move.location_dest_id.usage != 'internal' and move.product_id.cost_method == 'average':
- reference_amount = qty * move.product_id.standard_price
- elif move.product_id.cost_method == 'average' and move.price_unit:
- reference_amount = qty * move.price_unit
- reference_currency_id = move.price_currency_id.id or reference_currency_id
-
- # Otherwise we default to the company's valuation price type, considering that the values of the
- # valuation field are expressed in the default currency of the move's company.
- else:
- if context is None:
- context = {}
- currency_ctx = dict(context, currency_id = move.company_id.currency_id.id)
- amount_unit = move.product_id.price_get('standard_price', context=currency_ctx)[move.product_id.id]
- reference_amount = amount_unit * qty
-
- return reference_amount, reference_currency_id
-
-
- def _create_product_valuation_moves(self, cr, uid, move, context=None):
- """
- Generate the appropriate accounting moves if the product being moves is subject
- to real_time valuation tracking, and the source or destination location is
- a transit location or is outside of the company.
- """
- if move.product_id.valuation == 'real_time': # FIXME: product valuation should perhaps be a property?
- if context is None:
- context = {}
- src_company_ctx = dict(context,force_company=move.location_id.company_id.id)
- dest_company_ctx = dict(context,force_company=move.location_dest_id.company_id.id)
- account_moves = []
- # Outgoing moves (or cross-company output part)
- if move.location_id.company_id \
- and (move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal'\
- or move.location_id.company_id != move.location_dest_id.company_id):
- journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, src_company_ctx)
- reference_amount, reference_currency_id = self._get_reference_accounting_values_for_valuation(cr, uid, move, src_company_ctx)
- #returning goods to supplier
- if move.location_dest_id.usage == 'supplier':
- account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_valuation, acc_src, reference_amount, reference_currency_id, context))]
- else:
- account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_valuation, acc_dest, reference_amount, reference_currency_id, context))]
-
- # Incoming moves (or cross-company input part)
- if move.location_dest_id.company_id \
- and (move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal'\
- or move.location_id.company_id != move.location_dest_id.company_id):
- journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, dest_company_ctx)
- reference_amount, reference_currency_id = self._get_reference_accounting_values_for_valuation(cr, uid, move, src_company_ctx)
- #goods return from customer
- if move.location_id.usage == 'customer':
- account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_dest, acc_valuation, reference_amount, reference_currency_id, context))]
+ if move.state == 'done':
+ raise osv.except_osv(_('Operation Forbidden!'),
+ _('You cannot cancel a stock move that has been set to \'Done\'.'))
+ if move.reserved_quant_ids:
+ self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
+ if context.get('cancel_procurement'):
+ if move.propagate:
+ procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
+ procurement_obj.cancel(cr, uid, procurement_ids, context=context)
+ elif move.move_dest_id:
+ #cancel chained moves
+ if move.propagate:
+ self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
+ # If we have a long chain of moves to be cancelled, it is easier for the user to handle
+ # only the last procurement which will go into exception, instead of all procurements
+ # along the chain going into exception. We need to check if there are no split moves not cancelled however
+ if move.procurement_id:
+ proc = move.procurement_id
+ if all([x.state == 'cancel' for x in proc.move_ids if x.id != move.id]):
+ procurement_obj.write(cr, uid, [proc.id], {'state': 'cancel'})
+
+ elif move.move_dest_id.state == 'waiting':
+ self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context)
+ return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
+
+ def _check_package_from_moves(self, cr, uid, ids, context=None):
+ pack_obj = self.pool.get("stock.quant.package")
+ packs = set()
+ for move in self.browse(cr, uid, ids, context=context):
+ packs |= set([q.package_id for q in move.quant_ids if q.package_id and q.qty > 0])
+ return pack_obj._check_location_constraint(cr, uid, list(packs), context=context)
+
+ def find_move_ancestors(self, cr, uid, move, context=None):
+ '''Find the first level ancestors of given move '''
+ ancestors = []
+ move2 = move
+ while move2:
+ ancestors += [x.id for x in move2.move_orig_ids]
+ #loop on the split_from to find the ancestor of split moves only if the move has not direct ancestor (priority goes to them)
+ move2 = not move2.move_orig_ids and move2.split_from or False
+ return ancestors
+
+ def recalculate_move_state(self, cr, uid, move_ids, context=None):
+ '''Recompute the state of moves given because their reserved quants were used to fulfill another operation'''
+ for move in self.browse(cr, uid, move_ids, context=context):
+ vals = {}
+ reserved_quant_ids = move.reserved_quant_ids
+ if len(reserved_quant_ids) > 0 and not move.partially_available:
+ vals['partially_available'] = True
+ if len(reserved_quant_ids) == 0 and move.partially_available:
+ vals['partially_available'] = False
+ if move.state == 'assigned':
+ if self.find_move_ancestors(cr, uid, move, context=context):
+ vals['state'] = 'waiting'
else:
- account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_src, acc_valuation, reference_amount, reference_currency_id, context))]
-
- move_obj = self.pool.get('account.move')
- for j_id, move_lines in account_moves:
- move_obj.create(cr, uid,
- {
- 'journal_id': j_id,
- 'line_id': move_lines,
- 'ref': move.picking_id and move.picking_id.name}, context=context)
+ vals['state'] = 'confirmed'
+ if vals:
+ self.write(cr, uid, [move.id], vals, context=context)
def action_done(self, cr, uid, ids, context=None):
- """ Makes the move done and if all moves are done, it will finish the picking.
- @return:
+ """ Process completly the moves given as ids and if all moves are done, it will finish the picking.
"""
- picking_ids = []
- move_ids = []
- if context is None:
- context = {}
-
- todo = []
- for move in self.browse(cr, uid, ids, context=context):
- if move.state=="draft":
- todo.append(move.id)
+ context = context or {}
+ picking_obj = self.pool.get("stock.picking")
+ quant_obj = self.pool.get("stock.quant")
+ todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
if todo:
- self.action_confirm(cr, uid, todo, context=context)
- todo = []
-
+ ids = self.action_confirm(cr, uid, todo, context=context)
+ pickings = set()
+ procurement_ids = []
+ #Search operations that are linked to the moves
+ operations = set()
+ move_qty = {}
for move in self.browse(cr, uid, ids, context=context):
- if move.state in ['done','cancel']:
- continue
- move_ids.append(move.id)
-
- if move.picking_id:
- picking_ids.append(move.picking_id.id)
- if move.move_dest_id.id and (move.state != 'done'):
- # Downstream move should only be triggered if this move is the last pending upstream move
- other_upstream_move_ids = self.search(cr, uid, [('id','not in',move_ids),('state','not in',['done','cancel']),
- ('move_dest_id','=',move.move_dest_id.id)], context=context)
- if not other_upstream_move_ids:
- self.write(cr, uid, [move.id], {'move_history_ids': [(4, move.move_dest_id.id)]})
- if move.move_dest_id.state in ('waiting', 'confirmed'):
- self.force_assign(cr, uid, [move.move_dest_id.id], context=context)
- if move.move_dest_id.picking_id:
- workflow.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
- if move.move_dest_id.auto_validate:
- self.action_done(cr, uid, [move.move_dest_id.id], context=context)
-
- self._create_product_valuation_moves(cr, uid, move, context=context)
- if move.state not in ('confirmed','done','assigned'):
- todo.append(move.id)
-
- if todo:
- self.action_confirm(cr, uid, todo, context=context)
-
- self.write(cr, uid, move_ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
- for id in move_ids:
- workflow.trg_trigger(uid, 'stock.move', id, cr)
-
- for pick_id in picking_ids:
- workflow.trg_write(uid, 'stock.picking', pick_id, cr)
-
+ move_qty[move.id] = move.product_qty
+ for link in move.linked_move_operation_ids:
+ operations.add(link.operation_id)
+
+ #Sort operations according to entire packages first, then package + lot, package only, lot only
+ operations = list(operations)
+ 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))
+
+ for ops in operations:
+ if ops.picking_id:
+ pickings.add(ops.picking_id.id)
+ main_domain = [('qty', '>', 0)]
+ for record in ops.linked_move_operation_ids:
+ move = record.move_id
+ self.check_tracking(cr, uid, move, ops.package_id.id or ops.lot_id.id, context=context)
+ prefered_domain = [('reservation_id', '=', move.id)]
+ fallback_domain = [('reservation_id', '=', False)]
+ fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
+ prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
+ dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
+ quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain_list=prefered_domain_list,
+ restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
+ if ops.result_package_id.id:
+ #if a result package is given, all quants go there
+ quant_dest_package_id = ops.result_package_id.id
+ elif ops.product_id and ops.package_id:
+ #if a package and a product is given, we will remove quants from the pack.
+ quant_dest_package_id = False
+ else:
+ #otherwise we keep the current pack of the quant, which may mean None
+ quant_dest_package_id = ops.package_id.id
+ quant_obj.quants_move(cr, uid, quants, move, ops.location_dest_id, location_from=ops.location_id, lot_id=ops.lot_id.id, owner_id=ops.owner_id.id, src_package_id=ops.package_id.id, dest_package_id=quant_dest_package_id, context=context)
+ # Handle pack in pack
+ if not ops.product_id and ops.package_id and ops.result_package_id.id != ops.package_id.parent_id.id:
+ self.pool.get('stock.quant.package').write(cr, SUPERUSER_ID, [ops.package_id.id], {'parent_id': ops.result_package_id.id}, context=context)
+ move_qty[move.id] -= record.qty
+ #Check for remaining qtys and unreserve/check move_dest_id in
+ for move in self.browse(cr, uid, ids, context=context):
+ if move_qty[move.id] > 0: # (=In case no pack operations in picking)
+ main_domain = [('qty', '>', 0)]
+ prefered_domain = [('reservation_id', '=', move.id)]
+ fallback_domain = [('reservation_id', '=', False)]
+ fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
+ prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
+ self.check_tracking(cr, uid, move, move.restrict_lot_id.id, context=context)
+ qty = move_qty[move.id]
+ quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain, prefered_domain_list=prefered_domain_list, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
+ quant_obj.quants_move(cr, uid, quants, move, move.location_dest_id, lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id, context=context)
+ #unreserve the quants and make them available for other operations/moves
+ quant_obj.quants_unreserve(cr, uid, move, context=context)
+
+ #Check moves that were pushed
+ if move.move_dest_id.state in ('waiting', 'confirmed'):
++ # FIXME is opw 607970 still present with new WMS?
++ # (see commits 1ef2c181033bd200906fb1e5ce35e234bf566ac6
++ # and 41c5ceb8ebb95c1b4e98d8dd1f12b8e547a24b1d)
+ other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
+ ('move_dest_id', '=', move.move_dest_id.id)], context=context)
+ #If no other moves for the move that got pushed:
+ if not other_upstream_move_ids and move.move_dest_id.state in ('waiting', 'confirmed'):
+ self.action_assign(cr, uid, [move.move_dest_id.id], context=context)
+ if move.procurement_id:
+ procurement_ids.append(move.procurement_id.id)
+
+ # Check the packages have been placed in the correct locations
+ self._check_package_from_moves(cr, uid, ids, context=context)
+ #set the move as done
+ self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
+ self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
+ #check picking state to set the date_done is needed
+ done_picking = []
+ for picking in picking_obj.browse(cr, uid, list(pickings), context=context):
+ if picking.state == 'done' and not picking.date_done:
+ done_picking.append(picking.id)
+ if done_picking:
+ picking_obj.write(cr, uid, done_picking, {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
return True
- def _create_account_move_line(self, cr, uid, move, src_account_id, dest_account_id, reference_amount, reference_currency_id, context=None):
- """
- Generate the account.move.line values to post to track the stock valuation difference due to the
- processing of the given stock move.
- """
- # prepare default values considering that the destination accounts have the reference_currency_id as their main currency
- partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False
- debit_line_vals = {
- 'name': move.name,
- 'product_id': move.product_id and move.product_id.id or False,
- 'quantity': move.product_qty,
- 'ref': move.picking_id and move.picking_id.name or False,
- 'date': time.strftime('%Y-%m-%d'),
- 'partner_id': partner_id,
- 'debit': reference_amount,
- 'account_id': dest_account_id,
- }
- credit_line_vals = {
- 'name': move.name,
- 'product_id': move.product_id and move.product_id.id or False,
- 'quantity': move.product_qty,
- 'ref': move.picking_id and move.picking_id.name or False,
- 'date': time.strftime('%Y-%m-%d'),
- 'partner_id': partner_id,
- 'credit': reference_amount,
- 'account_id': src_account_id,
- }
-
- # if we are posting to accounts in a different currency, provide correct values in both currencies correctly
- # when compatible with the optional secondary currency on the account.
- # Financial Accounts only accept amounts in secondary currencies if there's no secondary currency on the account
- # or if it's the same as that of the secondary amount being posted.
- account_obj = self.pool.get('account.account')
- src_acct, dest_acct = account_obj.browse(cr, uid, [src_account_id, dest_account_id], context=context)
- src_main_currency_id = src_acct.company_id.currency_id.id
- dest_main_currency_id = dest_acct.company_id.currency_id.id
- cur_obj = self.pool.get('res.currency')
- if reference_currency_id != src_main_currency_id:
- # fix credit line:
- credit_line_vals['credit'] = cur_obj.compute(cr, uid, reference_currency_id, src_main_currency_id, reference_amount, context=context)
- if (not src_acct.currency_id) or src_acct.currency_id.id == reference_currency_id:
- credit_line_vals.update(currency_id=reference_currency_id, amount_currency=-reference_amount)
- if reference_currency_id != dest_main_currency_id:
- # fix debit line:
- debit_line_vals['debit'] = cur_obj.compute(cr, uid, reference_currency_id, dest_main_currency_id, reference_amount, context=context)
- if (not dest_acct.currency_id) or dest_acct.currency_id.id == reference_currency_id:
- debit_line_vals.update(currency_id=reference_currency_id, amount_currency=reference_amount)
-
- return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
-
def unlink(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
- ctx = context.copy()
+ context = context or {}
for move in self.browse(cr, uid, ids, context=context):
- if move.state != 'draft' and not ctx.get('call_unlink', False):
+ if move.state not in ('draft', 'cancel'):
raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
- return super(stock_move, self).unlink(
- cr, uid, ids, context=ctx)
-
- # _create_lot function is not used anywhere
- def _create_lot(self, cr, uid, ids, product_id, prefix=False):
- """ Creates production lot
- @return: Production lot id
- """
- prodlot_obj = self.pool.get('stock.production.lot')
- prodlot_id = prodlot_obj.create(cr, uid, {'prefix': prefix, 'product_id': product_id})
- return prodlot_id
+ return super(stock_move, self).unlink(cr, uid, ids, context=context)
- def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
+ def action_scrap(self, cr, uid, ids, quantity, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
""" Move the scrap/damaged product into scrap location
@param cr: the database cursor
@param uid: the user id