todo = []
for move in pick.move_lines:
if move.state == 'draft':
- self.pool.get('stock.move').action_confirm(cr, uid, [move.id],
- context=context)
- todo.append(move.id)
- elif move.state in ('assigned','confirmed'):
+ todo.extend(self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context))
+ elif move.state in ('assigned', 'confirmed'):
todo.append(move.id)
if len(todo):
- self.pool.get('stock.move').action_done(cr, uid, todo,
- context=context)
+ self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
return True
- def get_currency_id(self, cr, uid, picking):
- return False
-
- def _get_partner_to_invoice(self, cr, uid, picking, context=None):
- """ Gets the partner that will be invoiced
- Note that this function is inherited in the sale and purchase modules
- @param picking: object of the picking for which we are selecting the partner to invoice
- @return: object of the partner to invoice
- """
- return picking.partner_id and picking.partner_id.id
-
- def _get_comment_invoice(self, cr, uid, picking):
- """
- @return: comment string for invoice
- """
- return picking.note or ''
-
- def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None):
- """ Gets price unit for invoice
- @param move_line: Stock move lines
- @param type: Type of invoice
- @return: The price unit for the move line
- """
- if context is None:
- context = {}
-
- if type in ('in_invoice', 'in_refund'):
- # Take the user company and pricetype
- context['currency_id'] = move_line.company_id.currency_id.id
- amount_unit = move_line.product_id.price_get('standard_price', context=context)[move_line.product_id.id]
- return amount_unit
- else:
- return move_line.product_id.list_price
+ def unlink(self, cr, uid, ids, context=None):
+ #on picking deletion, cancel its move then unlink them too
+ move_obj = self.pool.get('stock.move')
+ context = context or {}
+ for pick in self.browse(cr, uid, ids, context=context):
+ move_ids = [move.id for move in pick.move_lines]
+ move_obj.action_cancel(cr, uid, move_ids, context=context)
+ move_obj.unlink(cr, uid, move_ids, context=context)
+ return super(stock_picking, self).unlink(cr, uid, ids, context=context)
- def _get_discount_invoice(self, cr, uid, move_line):
- '''Return the discount for the move line'''
- return 0.0
+ def write(self, cr, uid, ids, vals, context=None):
+ res = super(stock_picking, self).write(cr, uid, ids, vals, context=context)
+ #if we changed the move lines or the pack operations, we need to recompute the remaining quantities of both
+ if 'move_lines' in vals or 'pack_operation_ids' in vals:
+ self.do_recompute_remaining_quantities(cr, uid, ids, context=context)
+ return res
- def _get_taxes_invoice(self, cr, uid, move_line, type):
- """ Gets taxes on invoice
- @param move_line: Stock move lines
- @param type: Type of invoice
- @return: Taxes Ids for the move line
+ def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
+ """ 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.
"""
- if type in ('in_invoice', 'in_refund'):
- taxes = move_line.product_id.supplier_taxes_id
- else:
- taxes = move_line.product_id.taxes_id
-
- if move_line.picking_id and move_line.picking_id.partner_id and move_line.picking_id.partner_id.id:
- return self.pool.get('account.fiscal.position').map_tax(
- cr,
- uid,
- move_line.picking_id.partner_id.property_account_position,
- taxes
- )
- else:
- return map(lambda x: x.id, taxes)
-
- def _get_account_analytic_invoice(self, cr, uid, picking, move_line):
+ if not backorder_moves:
+ backorder_moves = picking.move_lines
+ backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done', 'cancel')]
+ if 'do_only_split' in context and context['do_only_split']:
+ backorder_move_ids = [x.id for x in backorder_moves if x.id not in context.get('split', [])]
+
+ if backorder_move_ids:
+ backorder_id = self.copy(cr, uid, picking.id, {
+ 'name': '/',
+ 'move_lines': [],
+ 'pack_operation_ids': [],
+ 'backorder_id': picking.id,
+ })
- back_order_name = self.browse(cr, uid, backorder_id, context=context).name
- self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (back_order_name), context=context)
++ self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (picking.name), context=context)
+ move_obj = self.pool.get("stock.move")
+ move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
+
+ self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
+ self.action_confirm(cr, uid, [backorder_id], context=context)
+ return backorder_id
return False
- def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id):
- '''Call after the creation of the invoice line'''
- return
-
- def _invoice_hook(self, cr, uid, picking, invoice_id):
- '''Call after the creation of the invoice'''
- return
-
- def _get_invoice_type(self, pick):
- src_usage = dest_usage = None
- inv_type = None
- if pick.invoice_state == '2binvoiced':
- if pick.move_lines:
- src_usage = pick.move_lines[0].location_id.usage
- dest_usage = pick.move_lines[0].location_dest_id.usage
- if pick.type == 'out' and dest_usage == 'supplier':
- inv_type = 'in_refund'
- elif pick.type == 'out' and dest_usage == 'customer':
- inv_type = 'out_invoice'
- elif pick.type == 'in' and src_usage == 'supplier':
- inv_type = 'in_invoice'
- elif pick.type == 'in' and src_usage == 'customer':
- inv_type = 'out_refund'
- else:
- inv_type = 'out_invoice'
- return inv_type
-
- def _prepare_invoice_group(self, cr, uid, picking, partner, invoice, context=None):
- """ Builds the dict for grouped invoices
- @param picking: picking object
- @param partner: object of the partner to invoice (not used here, but may be usefull if this function is inherited)
- @param invoice: object of the invoice that we are updating
- @return: dict that will be used to update the invoice
- """
- comment = self._get_comment_invoice(cr, uid, picking)
- return {
- 'name': (invoice.name or '') + ', ' + (picking.name or ''),
- 'origin': (invoice.origin or '') + ', ' + (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
- 'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''),
- 'date_invoice': context.get('date_inv', False),
- }
+ def recheck_availability(self, cr, uid, picking_ids, context=None):
+ self.action_assign(cr, uid, picking_ids, context=context)
+ self.do_prepare_partial(cr, uid, picking_ids, context=context)
- def _prepare_invoice(self, cr, uid, picking, partner, inv_type, journal_id, context=None):
- """ Builds the dict containing the values for the invoice
- @param picking: picking object
- @param partner: object of the partner to invoice
- @param inv_type: type of the invoice ('out_invoice', 'in_invoice', ...)
- @param journal_id: ID of the accounting journal
- @return: dict that will be used to create the invoice object
+ def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None):
+ """This method searches for the higher level packages that can be moved as a single operation, given a list of quants
+ to move and their suggested destination, and returns the list of matching packages.
"""
- if isinstance(partner, int):
- partner = self.pool.get('res.partner').browse(cr, uid, partner, context=context)
- if inv_type in ('out_invoice', 'out_refund'):
- account_id = partner.property_account_receivable.id
- payment_term = partner.property_payment_term.id or False
- else:
- account_id = partner.property_account_payable.id
- payment_term = partner.property_supplier_payment_term.id or False
- comment = self._get_comment_invoice(cr, uid, picking)
- invoice_vals = {
- 'name': picking.name,
- 'origin': (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
- 'type': inv_type,
- 'account_id': account_id,
- 'partner_id': partner.id,
- 'comment': comment,
- 'payment_term': payment_term,
- 'fiscal_position': partner.property_account_position.id,
- 'date_invoice': context.get('date_inv', False),
- 'company_id': picking.company_id.id,
- 'user_id': uid,
- }
- cur_id = self.get_currency_id(cr, uid, picking)
- if cur_id:
- invoice_vals['currency_id'] = cur_id
- if journal_id:
- invoice_vals['journal_id'] = journal_id
- return invoice_vals
-
- def _prepare_invoice_line(self, cr, uid, group, picking, move_line, invoice_id,
- invoice_vals, context=None):
- """ Builds the dict containing the values for the invoice line
- @param group: True or False
- @param picking: picking object
- @param: move_line: move_line object
- @param: invoice_id: ID of the related invoice
- @param: invoice_vals: dict used to created the invoice
- @return: dict that will be used to create the invoice line
- """
- if group:
- name = (picking.name or '') + '-' + move_line.name
- else:
- name = move_line.name
- origin = move_line.picking_id.name or ''
- if move_line.picking_id.origin:
- origin += ':' + move_line.picking_id.origin
-
- if invoice_vals['type'] in ('out_invoice', 'out_refund'):
- account_id = move_line.product_id.property_account_income.id
- if not account_id:
- account_id = move_line.product_id.categ_id.\
- property_account_income_categ.id
- else:
- account_id = move_line.product_id.property_account_expense.id
- if not account_id:
- account_id = move_line.product_id.categ_id.\
- property_account_expense_categ.id
- if invoice_vals['fiscal_position']:
- fp_obj = self.pool.get('account.fiscal.position')
- fiscal_position = fp_obj.browse(cr, uid, invoice_vals['fiscal_position'], context=context)
- account_id = fp_obj.map_account(cr, uid, fiscal_position, account_id)
- # set UoS if it's a sale and the picking doesn't have one
- uos_id = move_line.product_uos and move_line.product_uos.id or False
- if not uos_id and invoice_vals['type'] in ('out_invoice', 'out_refund'):
- uos_id = move_line.product_uom.id
-
- return {
- 'name': name,
- 'origin': origin,
- 'invoice_id': invoice_id,
- 'uos_id': uos_id,
- 'product_id': move_line.product_id.id,
- 'account_id': account_id,
- 'price_unit': self._get_price_unit_invoice(cr, uid, move_line, invoice_vals['type']),
- 'discount': self._get_discount_invoice(cr, uid, move_line),
- 'quantity': move_line.product_uos_qty or move_line.product_qty,
- 'invoice_line_tax_id': [(6, 0, self._get_taxes_invoice(cr, uid, move_line, invoice_vals['type']))],
- 'account_analytic_id': self._get_account_analytic_invoice(cr, uid, picking, move_line),
- }
-
- def action_invoice_create(self, cr, uid, ids, journal_id=False,
- group=False, type='out_invoice', context=None):
- """ Creates invoice based on the invoice state selected for picking.
- @param journal_id: Id of journal
- @param group: Whether to create a group invoice or not
- @param type: Type invoice to be created
- @return: Ids of created invoices for the pickings
+ # Try to find as much as possible top-level packages that can be moved
+ pack_obj = self.pool.get("stock.quant.package")
+ quant_obj = self.pool.get("stock.quant")
+ top_lvl_packages = set()
+ quants_to_compare = quants_suggested_locations.keys()
+ for pack in list(set([x.package_id for x in quants_suggested_locations.keys() if x and x.package_id])):
+ loop = True
+ test_pack = pack
+ good_pack = False
+ pack_destination = False
+ while loop:
+ pack_quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
+ all_in = True
+ for quant in quant_obj.browse(cr, uid, pack_quants, context=context):
+ # If the quant is not in the quants to compare and not in the common location
+ if not quant in quants_to_compare:
+ all_in = False
+ break
+ else:
+ #if putaway strat apply, the destination location of each quant may be different (and thus the package should not be taken as a single operation)
+ if not pack_destination:
+ pack_destination = quants_suggested_locations[quant]
+ elif pack_destination != quants_suggested_locations[quant]:
+ all_in = False
+ break
+ if all_in:
+ good_pack = test_pack
+ if test_pack.parent_id:
+ test_pack = test_pack.parent_id
+ else:
+ #stop the loop when there's no parent package anymore
+ loop = False
+ else:
+ #stop the loop when the package test_pack is not totally reserved for moves of this picking
+ #(some quants may be reserved for other picking or not reserved at all)
+ loop = False
+ if good_pack:
+ top_lvl_packages.add(good_pack)
+ return list(top_lvl_packages)
+
+ def _prepare_pack_ops(self, cr, uid, picking, quants, forced_qties, context=None):
+ """ returns a list of dict, ready to be used in create() of stock.pack.operation.
+
+ :param picking: browse record (stock.picking)
+ :param quants: browse record list (stock.quant). List of quants associated to the picking
+ :param forced_qties: dictionary showing for each product (keys) its corresponding quantity (value) that is not covered by the quants associated to the picking
"""
- if context is None:
- context = {}
+ def _picking_putaway_apply(product):
+ location = False
+ # Search putaway strategy
+ if product_putaway_strats.get(product.id):
+ location = product_putaway_strats[product.id]
+ else:
+ location = self.pool.get('stock.location').get_putaway_strategy(cr, uid, picking.location_dest_id, product, context=context)
+ product_putaway_strats[product.id] = location
+ return location or picking.location_dest_id.id
+
+ pack_obj = self.pool.get("stock.quant.package")
+ quant_obj = self.pool.get("stock.quant")
+ vals = []
+ qtys_grouped = {}
+ #for each quant of the picking, find the suggested location
+ quants_suggested_locations = {}
+ product_putaway_strats = {}
+ for quant in quants:
+ if quant.qty <= 0:
+ continue
+ suggested_location_id = _picking_putaway_apply(quant.product_id)
+ quants_suggested_locations[quant] = suggested_location_id
+
+ #find the packages we can movei as a whole
+ top_lvl_packages = self._get_top_level_packages(cr, uid, quants_suggested_locations, context=context)
+ # and then create pack operations for the top-level packages found
+ for pack in top_lvl_packages:
+ pack_quant_ids = pack_obj.get_content(cr, uid, [pack.id], context=context)
+ pack_quants = quant_obj.browse(cr, uid, pack_quant_ids, context=context)
+ vals.append({
+ 'picking_id': picking.id,
+ 'package_id': pack.id,
+ 'product_qty': 1.0,
+ 'location_id': pack.location_id.id,
+ 'location_dest_id': quants_suggested_locations[pack_quants[0]],
+ })
+ #remove the quants inside the package so that they are excluded from the rest of the computation
+ for quant in pack_quants:
+ del quants_suggested_locations[quant]
+
+ # Go through all remaining reserved quants and group by product, package, lot, owner, source location and dest location
+ for quant, dest_location_id in quants_suggested_locations.items():
+ key = (quant.product_id.id, quant.package_id.id, quant.lot_id.id, quant.owner_id.id, quant.location_id.id, dest_location_id)
+ if qtys_grouped.get(key):
+ qtys_grouped[key] += quant.qty
+ else:
+ qtys_grouped[key] = quant.qty
- invoice_obj = self.pool.get('account.invoice')
- invoice_line_obj = self.pool.get('account.invoice.line')
- partner_obj = self.pool.get('res.partner')
- invoices_group = {}
- res = {}
- inv_type = type
- for picking in self.browse(cr, uid, ids, context=context):
- if picking.invoice_state != '2binvoiced':
+ # Do the same for the forced quantities (in cases of force_assign or incomming shipment for example)
+ for product, qty in forced_qties.items():
+ if qty <= 0:
continue
- partner = self._get_partner_to_invoice(cr, uid, picking, context=context)
- if isinstance(partner, int):
- partner = partner_obj.browse(cr, uid, [partner], context=context)[0]
- if not partner:
- raise osv.except_osv(_('Error, no partner!'),
- _('Please put a partner on the picking list if you want to generate invoice.'))
-
- if not inv_type:
- inv_type = self._get_invoice_type(picking)
-
- invoice_vals = self._prepare_invoice(cr, uid, picking, partner, inv_type, journal_id, context=context)
- if group and partner.id in invoices_group:
- invoice_id = invoices_group[partner.id]
- invoice = invoice_obj.browse(cr, uid, invoice_id)
- invoice_vals_group = self._prepare_invoice_group(cr, uid, picking, partner, invoice, context=context)
- invoice_obj.write(cr, uid, [invoice_id], invoice_vals_group, context=context)
+ suggested_location_id = _picking_putaway_apply(product)
+ key = (product.id, False, False, False, picking.location_id.id, suggested_location_id)
+ if qtys_grouped.get(key):
+ qtys_grouped[key] += qty
else:
- invoice_id = invoice_obj.create(cr, uid, invoice_vals, context=context)
- invoices_group[partner.id] = invoice_id
- res[picking.id] = invoice_id
- for move_line in picking.move_lines:
- if move_line.state == 'cancel':
- continue
- if move_line.scrapped:
- # do no invoice scrapped products
+ qtys_grouped[key] = qty
+
+ # Create the necessary operations for the grouped quants and remaining qtys
+ for key, qty in qtys_grouped.items():
+ vals.append({
+ 'picking_id': picking.id,
+ 'product_qty': qty,
+ 'product_id': key[0],
+ 'package_id': key[1],
+ 'lot_id': key[2],
+ 'owner_id': key[3],
+ 'location_id': key[4],
+ 'location_dest_id': key[5],
+ 'product_uom_id': self.pool.get("product.product").browse(cr, uid, key[0], context=context).uom_id.id,
+ })
+ return vals
+
+ def open_barcode_interface(self, cr, uid, picking_ids, context=None):
+ final_url="/barcode/web/#action=stock.ui&picking_id="+str(picking_ids[0])
+ return {'type': 'ir.actions.act_url', 'url':final_url, 'target': 'self',}
+
+ def do_partial_open_barcode(self, cr, uid, picking_ids, context=None):
+ self.do_prepare_partial(cr, uid, picking_ids, context=context)
+ return self.open_barcode_interface(cr, uid, picking_ids, context=context)
+
+ def do_prepare_partial(self, cr, uid, picking_ids, context=None):
+ context = context or {}
+ pack_operation_obj = self.pool.get('stock.pack.operation')
+ #used to avoid recomputing the remaining quantities at each new pack operation created
+ ctx = context.copy()
+ ctx['no_recompute'] = True
+
+ #get list of existing operations and delete them
+ existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context)
+ if existing_package_ids:
+ pack_operation_obj.unlink(cr, uid, existing_package_ids, context)
+ for picking in self.browse(cr, uid, picking_ids, context=context):
+ forced_qties = {} # Quantity remaining after calculating reserved quants
+ picking_quants = []
+ #Calculate packages, reserved quants, qtys of this picking's moves
+ for move in picking.move_lines:
+ if move.state not in ('assigned', 'confirmed'):
continue
- vals = self._prepare_invoice_line(cr, uid, group, picking, move_line,
- invoice_id, invoice_vals, context=context)
- if vals:
- invoice_line_id = invoice_line_obj.create(cr, uid, vals, context=context)
- self._invoice_line_hook(cr, uid, move_line, invoice_line_id)
-
- invoice_obj.button_compute(cr, uid, [invoice_id], context=context,
- set_total=(inv_type in ('in_invoice', 'in_refund')))
- self.write(cr, uid, [picking.id], {
- 'invoice_state': 'invoiced',
- }, context=context)
- self._invoice_hook(cr, uid, picking, invoice_id)
- self.write(cr, uid, res.keys(), {
- 'invoice_state': 'invoiced',
- }, context=context)
- return res
-
- def test_done(self, cr, uid, ids, context=None):
- """ Test whether the move lines are done or not.
- @return: True or False
+ move_quants = move.reserved_quant_ids
+ picking_quants += move_quants
+ forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0
+ #if we used force_assign() on the move, or if the move is incomming, forced_qty > 0
+ if forced_qty:
+ if forced_qties.get(move.product_id):
+ forced_qties[move.product_id] += forced_qty
+ else:
+ forced_qties[move.product_id] = forced_qty
+ for vals in self._prepare_pack_ops(cr, uid, picking, picking_quants, forced_qties, context=context):
+ pack_operation_obj.create(cr, uid, vals, context=ctx)
+ #recompute the remaining quantities all at once
+ self.do_recompute_remaining_quantities(cr, uid, picking_ids, context=context)
+ self.write(cr, uid, picking_ids, {'recompute_pack_op': False}, context=context)
+
+ def do_unreserve(self, cr, uid, picking_ids, context=None):
"""
- ok = False
- for pick in self.browse(cr, uid, ids, context=context):
- if not pick.move_lines:
- return True
- for move in pick.move_lines:
- if move.state not in ('cancel','done'):
- return False
- if move.state=='done':
- ok = True
- return ok
-
- def test_cancel(self, cr, uid, ids, context=None):
- """ Test whether the move lines are canceled or not.
- @return: True or False
+ Will remove all quants for picking in picking_ids
"""
- for pick in self.browse(cr, uid, ids, context=context):
- for move in pick.move_lines:
- if move.state not in ('cancel',):
- return False
- return True
-
- def allow_cancel(self, cr, uid, ids, context=None):
- for pick in self.browse(cr, uid, ids, context=context):
- if not pick.move_lines:
- return True
- for move in pick.move_lines:
- if move.state == 'done':
- raise osv.except_osv(_('Error!'), _('You cannot cancel the picking as some moves have been done. You should cancel the picking lines.'))
- return True
-
- def unlink(self, cr, uid, ids, context=None):
- move_obj = self.pool.get('stock.move')
- if context is None:
- context = {}
- for pick in self.browse(cr, uid, ids, context=context):
- if pick.state in ['done','cancel']:
- raise osv.except_osv(_('Error!'), _('You cannot remove the picking which is in %s state!')%(pick.state,))
+ moves_to_unreserve = []
+ pack_line_to_unreserve = []
+ for picking in self.browse(cr, uid, picking_ids, context=context):
+ moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')]
+ pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids]
+ if moves_to_unreserve:
+ if pack_line_to_unreserve:
+ self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
+ self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context)
+
+ def recompute_remaining_qty(self, cr, uid, picking, context=None):
+ def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False):
+ move_dict = prod2move_ids[product_id][index]
+ qty_on_link = min(move_dict['remaining_qty'], qty_to_assign)
+ self.pool.get('stock.move.operation.link').create(cr, uid, {'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id}, context=context)
+ if move_dict['remaining_qty'] == qty_on_link:
+ prod2move_ids[product_id].pop(index)
else:
- ids2 = [move.id for move in pick.move_lines]
- ctx = context.copy()
- ctx.update({'call_unlink':True})
- if pick.state != 'draft':
- #Cancelling the move in order to affect Virtual stock of product
- move_obj.action_cancel(cr, uid, ids2, ctx)
- #Removing the move
- move_obj.unlink(cr, uid, ids2, ctx)
-
- return super(stock_picking, self).unlink(cr, uid, ids, context=context)
+ move_dict['remaining_qty'] -= qty_on_link
+ return qty_on_link
+
+ def _create_link_for_quant(operation_id, quant, qty):
+ """create a link for given operation and reserved move of given quant, for the max quantity possible, and returns this quantity"""
+ if not quant.reservation_id.id:
+ return _create_link_for_product(operation_id, quant.product_id.id, qty)
+ qty_on_link = 0
+ for i in range(0, len(prod2move_ids[quant.product_id.id])):
+ if prod2move_ids[quant.product_id.id][i]['move'].id != quant.reservation_id.id:
+ continue
+ qty_on_link = _create_link_for_index(operation_id, i, quant.product_id.id, qty, quant_id=quant.id)
+ break
+ return qty_on_link
+
+ def _create_link_for_product(operation_id, product_id, qty):
+ '''method that creates the link between a given operation and move(s) of given product, for the given quantity.
+ Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
+ qty_to_assign = qty
+ if prod2move_ids.get(product_id):
+ while prod2move_ids[product_id] and qty_to_assign > 0:
+ qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False)
+ qty_to_assign -= qty_on_link
+ return qty_to_assign == 0
- # FIXME: needs refactoring, this code is partially duplicated in stock_move.do_partial()!
- def do_partial(self, cr, uid, ids, partial_datas, context=None):
- """ Makes partial picking and moves done.
- @param partial_datas : Dictionary containing details of partial picking
- like partner_id, partner_id, delivery_date,
- delivery moves with product_id, product_qty, uom
- @return: Dictionary of values
- """
- if context is None:
- context = {}
- else:
- context = dict(context)
- res = {}
- move_obj = self.pool.get('stock.move')
- product_obj = self.pool.get('product.product')
- currency_obj = self.pool.get('res.currency')
uom_obj = self.pool.get('product.uom')
- sequence_obj = self.pool.get('ir.sequence')
- for pick in self.browse(cr, uid, ids, context=context):
- new_picking = None
- complete, too_many, too_few = [], [], []
- move_product_qty, prodlot_ids, product_avail, partial_qty, product_uoms = {}, {}, {}, {}, {}
- for move in pick.move_lines:
- if move.state in ('done', 'cancel'):
- continue
- partial_data = partial_datas.get('move%s'%(move.id), {})
- product_qty = partial_data.get('product_qty',0.0)
- move_product_qty[move.id] = product_qty
- product_uom = partial_data.get('product_uom',False)
- product_price = partial_data.get('product_price',0.0)
- product_currency = partial_data.get('product_currency',False)
- prodlot_id = partial_data.get('prodlot_id')
- prodlot_ids[move.id] = prodlot_id
- product_uoms[move.id] = product_uom
- partial_qty[move.id] = uom_obj._compute_qty(cr, uid, product_uoms[move.id], product_qty, move.product_uom.id)
- if move.product_qty == partial_qty[move.id]:
- complete.append(move)
- elif move.product_qty > partial_qty[move.id]:
- too_few.append(move)
- else:
- too_many.append(move)
-
- # Average price computation
- if (pick.type == 'in') and (move.product_id.cost_method == 'average'):
- product = product_obj.browse(cr, uid, move.product_id.id)
- move_currency_id = move.company_id.currency_id.id
- context['currency_id'] = move_currency_id
- qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
-
- if product.id not in product_avail:
- # keep track of stock on hand including processed lines not yet marked as done
- product_avail[product.id] = product.qty_available
-
- if qty > 0:
- new_price = currency_obj.compute(cr, uid, product_currency,
- move_currency_id, product_price, round=False)
- new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
- product.uom_id.id)
- if product_avail[product.id] <= 0:
- product_avail[product.id] = 0
- new_std_price = new_price
+ package_obj = self.pool.get('stock.quant.package')
+ quant_obj = self.pool.get('stock.quant')
+ quants_in_package_done = set()
+ prod2move_ids = {}
+ still_to_do = []
+ #make a dictionary giving for each product, the moves and related quantity that can be used in operation links
+ for move in picking.move_lines:
+ if not prod2move_ids.get(move.product_id.id):
+ prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}]
+ else:
+ prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty})
+
+ need_rereserve = False
+ #sort the operations in order to give higher priority to those with a package, then a serial number
+ operations = picking.pack_operation_ids
+ 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))
+ #delete existing operations to start again from scratch
+ cr.execute("DELETE FROM stock_move_operation_link WHERE operation_id in %s", (tuple([x.id for x in operations]),))
+
+ #1) first, try to create links when quants can be identified without any doubt
+ for ops in operations:
+ #for each operation, create the links with the stock move by seeking on the matching reserved quants,
+ #and deffer the operation if there is some ambiguity on the move to select
+ if ops.package_id and not ops.product_id:
+ #entire package
+ quant_ids = package_obj.get_content(cr, uid, [ops.package_id.id], context=context)
+ for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
+ remaining_qty_on_quant = quant.qty
+ if quant.reservation_id:
+ #avoid quants being counted twice
+ quants_in_package_done.add(quant.id)
+ qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty)
+ remaining_qty_on_quant -= qty_on_link
+ if remaining_qty_on_quant:
+ still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant))
+ need_rereserve = True
+ elif ops.product_id.id:
+ #Check moves with same product
+ qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
+ for move_dict in prod2move_ids.get(ops.product_id.id, []):
+ move = move_dict['move']
+ for quant in move.reserved_quant_ids:
+ if not qty_to_assign > 0:
+ break
+ if quant.id in quants_in_package_done:
+ continue
+
+ #check if the quant is matching the operation details
+ if ops.package_id:
+ 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
else:
- # Get the standard price
- amount_unit = product.price_get('standard_price', context=context)[product.id]
- new_std_price = ((amount_unit * product_avail[product.id])\
- + (new_price * qty))/(product_avail[product.id] + qty)
- # Write the field according to price type field
- product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
-
- # Record the values that were chosen in the wizard, so they can be
- # used for inventory valuation if real-time valuation is enabled.
- move_obj.write(cr, uid, [move.id],
- {'price_unit': product_price,
- 'price_currency_id': product_currency})
-
- product_avail[product.id] += qty
-
-
-
- for move in too_few:
- product_qty = move_product_qty[move.id]
- if not new_picking:
- new_picking_name = pick.name
- self.write(cr, uid, [pick.id],
- {'name': sequence_obj.get(cr, uid,
- 'stock.picking.%s'%(pick.type)),
- })
- pick.refresh()
- new_picking = self.copy(cr, uid, pick.id,
- {
- 'name': new_picking_name,
- 'move_lines' : [],
- 'state':'draft',
- })
- if product_qty != 0:
- defaults = {
- 'product_qty' : product_qty,
- 'product_uos_qty': product_qty, #TODO: put correct uos_qty
- 'picking_id' : new_picking,
- 'state': 'assigned',
- 'move_dest_id': False,
- 'price_unit': move.price_unit,
- 'product_uom': product_uoms[move.id]
+ flag = not quant.package_id.id
+ flag = flag and ((ops.lot_id and ops.lot_id.id == quant.lot_id.id) or not ops.lot_id)
+ flag = flag and (ops.owner_id.id == quant.owner_id.id)
+ if flag:
+ max_qty_on_link = min(quant.qty, qty_to_assign)
+ qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
+ qty_to_assign -= qty_on_link
+ if qty_to_assign > 0:
+ #qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
+ #all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
+ still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
+ need_rereserve = True
+
+ #2) then, process the remaining part
+ all_op_processed = True
+ for ops, product_id, remaining_qty in still_to_do:
+ all_op_processed = all_op_processed and _create_link_for_product(ops.id, product_id, remaining_qty)
+ return (need_rereserve, all_op_processed)
+
+ def picking_recompute_remaining_quantities(self, cr, uid, picking, context=None):
+ need_rereserve = False
+ all_op_processed = True
+ if picking.pack_operation_ids:
+ need_rereserve, all_op_processed = self.recompute_remaining_qty(cr, uid, picking, context=context)
+ return need_rereserve, all_op_processed
+
+ def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None):
+ for picking in self.browse(cr, uid, picking_ids, context=context):
+ if picking.pack_operation_ids:
+ self.recompute_remaining_qty(cr, uid, picking, context=context)
+
+ def _create_extra_moves(self, cr, uid, picking, context=None):
+ '''This function creates move lines on a picking, at the time of do_transfer, based on
+ unexpected product transfers (or exceeding quantities) found in the pack operations.
+ '''
+ move_obj = self.pool.get('stock.move')
+ operation_obj = self.pool.get('stock.pack.operation')
+ moves = []
+ for op in picking.pack_operation_ids:
+ for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items():
+ if remaining_qty > 0:
+ product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+ vals = {
+ 'picking_id': picking.id,
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
+ 'product_id': product_id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': remaining_qty,
+ 'name': _('Extra Move: ') + product.name,
+ 'state': 'draft',
}
- prodlot_id = prodlot_ids[move.id]
- if prodlot_id:
- defaults.update(prodlot_id=prodlot_id)
- move_obj.copy(cr, uid, move.id, defaults)
- move_obj.write(cr, uid, [move.id],
- {
- 'product_qty': move.product_qty - partial_qty[move.id],
- 'product_uos_qty': move.product_qty - partial_qty[move.id], #TODO: put correct uos_qty
- 'prodlot_id': False,
- 'tracking_id': False,
- })
-
- if new_picking:
- move_obj.write(cr, uid, [c.id for c in complete], {'picking_id': new_picking})
- for move in complete:
- defaults = {'product_uom': product_uoms[move.id], 'product_qty': move_product_qty[move.id]}
- if prodlot_ids.get(move.id):
- defaults.update({'prodlot_id': prodlot_ids[move.id]})
- move_obj.write(cr, uid, [move.id], defaults)
- for move in too_many:
- product_qty = move_product_qty[move.id]
- defaults = {
- 'product_qty' : product_qty,
- 'product_uos_qty': product_qty, #TODO: put correct uos_qty
- 'product_uom': product_uoms[move.id]
- }
- prodlot_id = prodlot_ids.get(move.id)
- if prodlot_ids.get(move.id):
- defaults.update(prodlot_id=prodlot_id)
- if new_picking:
- defaults.update(picking_id=new_picking)
- move_obj.write(cr, uid, [move.id], defaults)
-
- # At first we confirm the new picking (if necessary)
- if new_picking:
- self.signal_button_confirm(cr, uid, [new_picking])
- # Then we finish the good picking
- self.write(cr, uid, [pick.id], {'backorder_id': new_picking})
- self.action_move(cr, uid, [new_picking], context=context)
- self.signal_button_done(cr, uid, [new_picking])
- workflow.trg_write(uid, 'stock.picking', pick.id, cr)
- delivered_pack_id = new_picking
- self.message_post(cr, uid, new_picking, body=_("Back order <em>%s</em> has been <b>created</b>.") % (pick.name), context=context)
- else:
- self.action_move(cr, uid, [pick.id], context=context)
- self.signal_button_done(cr, uid, [pick.id])
- delivered_pack_id = pick.id
-
- delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context)
- res[pick.id] = {'delivered_picking': delivered_pack.id or False}
-
- return res
-
- # views associated to each picking type
- _VIEW_LIST = {
- 'out': 'view_picking_out_form',
- 'in': 'view_picking_in_form',
- 'internal': 'view_picking_form',
- }
- def _get_view_id(self, cr, uid, type):
- """Get the view id suiting the given type
-
- @param type: the picking type as a string
- @return: view i, or False if no view found
- """
- res = self.pool.get('ir.model.data').get_object_reference(cr, uid,
- 'stock', self._VIEW_LIST.get(type, 'view_picking_form'))
- return res and res[1] or False
-
-
-class stock_production_lot(osv.osv):
-
- def name_get(self, cr, uid, ids, context=None):
- if not ids:
- return []
- reads = self.read(cr, uid, ids, ['name', 'prefix', 'ref'], context)
- res = []
- for record in reads:
- name = record['name']
- prefix = record['prefix']
- if prefix:
- name = prefix + '/' + name
- if record['ref']:
- name = '%s [%s]' % (name, record['ref'])
- res.append((record['id'], name))
- return res
+ moves.append(move_obj.create(cr, uid, vals, context=context))
+ if moves:
+ move_obj.action_confirm(cr, uid, moves, context=context)
+ return moves
- def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
- args = args or []
- ids = []
- if name:
- ids = self.search(cr, uid, [('prefix', '=', name)] + args, limit=limit, context=context)
- if not ids:
- ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
+ def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
+ """ Unreserve quants then try to reassign quants."""
+ stock_move_obj = self.pool.get('stock.move')
+ if not move_ids:
+ self.do_unreserve(cr, uid, [picking.id], context=context)
+ self.action_assign(cr, uid, [picking.id], context=context)
else:
- ids = self.search(cr, uid, args, limit=limit, context=context)
- return self.name_get(cr, uid, ids, context)
+ stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
+ stock_move_obj.action_assign(cr, uid, move_ids, context=context)
- _name = 'stock.production.lot'
- _description = 'Serial Number'
-
- def _get_stock(self, cr, uid, ids, field_name, arg, context=None):
- """ Gets stock of products for locations
- @return: Dictionary of values
+ def do_transfer(self, cr, uid, picking_ids, context=None):
+ """
+ If no pack operation, we do simple action_done of the picking
+ Otherwise, do the pack operations
"""
+ if not context:
+ context = {}
+ stock_move_obj = self.pool.get('stock.move')
+ for picking in self.browse(cr, uid, picking_ids, context=context):
+ if not picking.pack_operation_ids:
+ self.action_done(cr, uid, [picking.id], context=context)
+ continue
+ else:
+ need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, picking, context=context)
+ #create extra moves in the picking (unexpected product moves coming from pack operations)
+ todo_move_ids = []
+ if not all_op_processed:
+ todo_move_ids += self._create_extra_moves(cr, uid, picking, context=context)
+
+ picking.refresh()
+ #split move lines eventually
+
+ toassign_move_ids = []
+ for move in picking.move_lines:
+ remaining_qty = move.remaining_qty
+ if move.state in ('done', 'cancel'):
+ #ignore stock moves cancelled or already done
+ continue
+ elif move.state == 'draft':
+ toassign_move_ids.append(move.id)
+ if remaining_qty == 0:
+ if move.state in ('draft', 'assigned', 'confirmed'):
+ todo_move_ids.append(move.id)
+ elif remaining_qty > 0 and remaining_qty < move.product_qty:
+ new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context)
+ todo_move_ids.append(move.id)
+ #Assign move as it was assigned before
+ toassign_move_ids.append(new_move)
+ if (need_rereserve or not all_op_processed) and not picking.location_id.usage in ("supplier", "production", "inventory"):
+ self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context)
+ self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
+ if todo_move_ids and not context.get('do_only_split'):
+ self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context)
+ elif context.get('do_only_split'):
+ context.update({'split': todo_move_ids})
+ picking.refresh()
+ self._create_backorder(cr, uid, picking, context=context)
+ if toassign_move_ids:
+ stock_move_obj.action_assign(cr, uid, toassign_move_ids, context=context)
+ return True
+
+ def do_split(self, cr, uid, picking_ids, context=None):
+ """ just split the picking (create a backorder) without making it 'done' """
if context is None:
context = {}
- if 'location_id' not in context:
- locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
- else:
- locations = context['location_id'] and [context['location_id']] or []
+ ctx = context.copy()
+ ctx['do_only_split'] = True
+ return self.do_transfer(cr, uid, picking_ids, context=ctx)
- if isinstance(ids, (int, long)):
- ids = [ids]
+ def get_next_picking_for_ui(self, cr, uid, context=None):
+ """ returns the next pickings to process. Used in the barcode scanner UI"""
+ if context is None:
+ context = {}
+ domain = [('state', 'in', ('assigned', 'partially_available'))]
+ if context.get('default_picking_type_id'):
+ domain.append(('picking_type_id', '=', context['default_picking_type_id']))
+ return self.search(cr, uid, domain, context=context)
+
+ def action_done_from_ui(self, cr, uid, picking_id, context=None):
+ """ called when button 'done' is pushed in the barcode scanner UI """
+ #write qty_done into field product_qty for every package_operation before doing the transfer
+ pack_op_obj = self.pool.get('stock.pack.operation')
+ for operation in self.browse(cr, uid, picking_id, context=context).pack_operation_ids:
+ pack_op_obj.write(cr, uid, operation.id, {'product_qty': operation.qty_done}, context=context)
+ self.do_transfer(cr, uid, [picking_id], context=context)
+ #return id of next picking to work on
+ return self.get_next_picking_for_ui(cr, uid, context=context)
+
+ def action_pack(self, cr, uid, picking_ids, operation_filter_ids=None, context=None):
+ """ Create a package with the current pack_operation_ids of the picking that aren't yet in a pack.
+ Used in the barcode scanner UI and the normal interface as well.
+ operation_filter_ids is used by barcode scanner interface to specify a subset of operation to pack"""
+ if operation_filter_ids == None:
+ operation_filter_ids = []
+ stock_operation_obj = self.pool.get('stock.pack.operation')
+ package_obj = self.pool.get('stock.quant.package')
+ stock_move_obj = self.pool.get('stock.move')
+ for picking_id in picking_ids:
+ operation_search_domain = [('picking_id', '=', picking_id), ('result_package_id', '=', False)]
+ if operation_filter_ids != []:
+ operation_search_domain.append(('id', 'in', operation_filter_ids))
+ operation_ids = stock_operation_obj.search(cr, uid, operation_search_domain, context=context)
+ pack_operation_ids = []
+ if operation_ids:
+ for operation in stock_operation_obj.browse(cr, uid, operation_ids, context=context):
+ #If we haven't done all qty in operation, we have to split into 2 operation
+ op = operation
+ if (operation.qty_done < operation.product_qty):
+ new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context)
+ stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0, 'lot_id': False}, context=context)
+ op = stock_operation_obj.browse(cr, uid, new_operation, context=context)
+ pack_operation_ids.append(op.id)
+ for record in op.linked_move_operation_ids:
+ stock_move_obj.check_tracking(cr, uid, record.move_id, op.package_id.id or op.lot_id.id, context=context)
+ package_id = package_obj.create(cr, uid, {}, context=context)
+ stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context)
+ return True
- res = {}.fromkeys(ids, 0.0)
- if locations:
- cr.execute('''select
- prodlot_id,
- sum(qty)
- from
- stock_report_prodlots
- where
- location_id IN %s and prodlot_id IN %s group by prodlot_id''',(tuple(locations),tuple(ids),))
- res.update(dict(cr.fetchall()))
+ def process_product_id_from_ui(self, cr, uid, picking_id, product_id, op_id, increment=True, context=None):
+ return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id),('id', '=', op_id)], increment=increment, context=context)
- return res
+ def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, visible_op_ids, context=None):
+ '''This function is called each time there barcode scanner reads an input'''
+ lot_obj = self.pool.get('stock.production.lot')
+ package_obj = self.pool.get('stock.quant.package')
+ product_obj = self.pool.get('product.product')
+ stock_operation_obj = self.pool.get('stock.pack.operation')
+ stock_location_obj = self.pool.get('stock.location')
+ answer = {'filter_loc': False, 'operation_id': False}
+ #check if the barcode correspond to a location
+ matching_location_ids = stock_location_obj.search(cr, uid, [('loc_barcode', '=', barcode_str)], context=context)
+ if matching_location_ids:
+ #if we have a location, return immediatly with the location name
+ location = stock_location_obj.browse(cr, uid, matching_location_ids[0], context=None)
+ answer['filter_loc'] = stock_location_obj._name_get(cr, uid, location, context=None)
+ answer['filter_loc_id'] = matching_location_ids[0]
+ return answer
+ #check if the barcode correspond to a product
+ matching_product_ids = product_obj.search(cr, uid, ['|', ('ean13', '=', barcode_str), ('default_code', '=', barcode_str)], context=context)
+ if matching_product_ids:
+ op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', matching_product_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
+ answer['operation_id'] = op_id
+ return answer
+ #check if the barcode correspond to a lot
+ matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
+ if matching_lot_ids:
+ lot = lot_obj.browse(cr, uid, matching_lot_ids[0], context=context)
+ op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
+ answer['operation_id'] = op_id
+ return answer
+ #check if the barcode correspond to a package
+ matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
+ if matching_package_ids:
+ op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
+ answer['operation_id'] = op_id
+ return answer
+ return answer
- def _stock_search(self, cr, uid, obj, name, args, context=None):
- """ Searches Ids of products
- @return: Ids of locations
- """
- locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')])
- cr.execute('''select
- prodlot_id,
- sum(qty)
- from
- stock_report_prodlots
- where
- location_id IN %s group by prodlot_id
- having sum(qty) '''+ str(args[0][1]) + str(args[0][2]),(tuple(locations),))
- res = cr.fetchall()
- ids = [('id', 'in', map(lambda x: x[0], res))]
- return ids
+class stock_production_lot(osv.osv):
+ _name = 'stock.production.lot'
+ _inherit = ['mail.thread']
+ _description = 'Lot/Serial'
_columns = {
- 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number, will be displayed as: PREFIX/SERIAL [INT_REF]"),
+ 'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number"),
'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"),
- 'prefix': fields.char('Prefix', size=64, help="Optional prefix to prepend when displaying this serial number: PREFIX/SERIAL [INT_REF]"),
'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
- 'date': fields.datetime('Creation Date', required=True),
- 'stock_available': fields.function(_get_stock, fnct_search=_stock_search, type="float", string="Available", select=True,
- help="Current quantity of products with this Serial Number available in company warehouses",
- digits_compute=dp.get_precision('Product Unit of Measure')),
- 'revisions': fields.one2many('stock.production.lot.revision', 'lot_id', 'Revisions'),
- 'company_id': fields.many2one('res.company', 'Company', select=True),
- 'move_ids': fields.one2many('stock.move', 'prodlot_id', 'Moves for this serial number', readonly=True),
+ 'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants', readonly=True),
+ 'create_date': fields.datetime('Creation Date'),
}
_defaults = {
- 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
'product_id': lambda x, y, z, c: c.get('product_id', False),
}
_sql_constraints = [
- ('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'),
+ ('name_ref_uniq', 'unique (name, ref, product_id, company_id)', 'The combination of Serial Number, internal reference, Product and Company must be unique !'),
]
+
def action_traceability(self, cr, uid, ids, context=None):
- """ It traces the information of a product
+ """ It traces the information of lots
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in