[MERGE] Merge from trunk-wms-loconopmerge2 + only keep good optims

bzr revid: jco@openerp.com-20140313170620-fq25a32fdlo3xrxy
This commit is contained in:
Josse Colpaert 2014-03-13 18:06:20 +01:00
commit dcb12c7db8
11 changed files with 417 additions and 259 deletions

View File

@ -208,7 +208,7 @@ class mail_message(osv.Model):
raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias.")) raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
def _get_default_author(self, cr, uid, context=None): def _get_default_author(self, cr, uid, context=None):
return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id return self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0]
_defaults = { _defaults = {
'type': 'email', 'type': 'email',

View File

@ -807,7 +807,10 @@ class product_product(osv.osv):
result = [] result = []
for product in self.browse(cr, SUPERUSER_ID, ids, context=context): for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids) if partner_id:
sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
else:
sellers = False
if sellers: if sellers:
for s in sellers: for s in sellers:
mydict = { mydict = {

View File

@ -31,6 +31,7 @@ import openerp.addons.decimal_precision as dp
from openerp.osv.orm import browse_record, browse_null from openerp.osv.orm import browse_record, browse_null
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
class purchase_order(osv.osv): class purchase_order(osv.osv):
def _amount_all(self, cr, uid, ids, field_name, arg, context=None): def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
@ -155,12 +156,19 @@ class purchase_order(osv.osv):
def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None): def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
res = {} res = {}
for purchase_id in ids: query = """
picking_ids = set() SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po
move_ids = self.pool.get('stock.move').search(cr, uid, [('purchase_line_id.order_id', '=', purchase_id)], context=context) WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id
for move in self.pool.get('stock.move').browse(cr, uid, move_ids, context=context): GROUP BY picking_id, po.id
picking_ids.add(move.picking_id.id)
res[purchase_id] = list(picking_ids) """
cr.execute(query, (tuple(ids), ))
picks = cr.fetchall()
for pick in picks:
if not res.get(pick[1]):
res[pick[1]] = [pick[0]]
else:
res[pick[1]].append(pick[0])
return res return res
STATE_SELECTION = [ STATE_SELECTION = [

View File

@ -35,7 +35,7 @@ class stock_move(osv.osv):
ids = [ids] ids = [ids]
res = super(stock_move, self).write(cr, uid, ids, vals, context=context) res = super(stock_move, self).write(cr, uid, ids, vals, context=context)
from openerp import workflow from openerp import workflow
if 'state' in vals: if 'state' in vals and vals['state'] in ['done', 'cancel']:
for move in self.browse(cr, uid, ids, context=context): for move in self.browse(cr, uid, ids, context=context):
if move.purchase_line_id and move.purchase_line_id.order_id: if move.purchase_line_id and move.purchase_line_id.order_id:
order_id = move.purchase_line_id.order_id.id order_id = move.purchase_line_id.order_id.id
@ -71,7 +71,7 @@ class stock_picking(osv.osv):
def _get_picking_to_recompute(self, cr, uid, ids, context=None): def _get_picking_to_recompute(self, cr, uid, ids, context=None):
picking_ids = set() picking_ids = set()
for move in self.pool.get('stock.move').browse(cr, uid, ids, context=context): for move in self.pool.get('stock.move').browse(cr, uid, ids, context=context):
if move.picking_id: if move.picking_id and move.purchase_line_id:
picking_ids.add(move.picking_id.id) picking_ids.add(move.picking_id.id)
return list(picking_ids) return list(picking_ids)
@ -79,7 +79,6 @@ class stock_picking(osv.osv):
'reception_to_invoice': fields.function(_get_to_invoice, type='boolean', string='Invoiceable on incoming shipment?', 'reception_to_invoice': fields.function(_get_to_invoice, type='boolean', string='Invoiceable on incoming shipment?',
help='Does the picking contains some moves related to a purchase order invoiceable on the reception?', help='Does the picking contains some moves related to a purchase order invoiceable on the reception?',
store={ store={
'stock.picking': (lambda self, cr, uid, ids, c={}: ids, ['move_lines'], 10),
'stock.move': (_get_picking_to_recompute, ['purchase_line_id', 'picking_id'], 10), 'stock.move': (_get_picking_to_recompute, ['purchase_line_id', 'picking_id'], 10),
}), }),
} }

View File

@ -4,4 +4,4 @@
I duplicate order. I duplicate order.
- -
!python {model: purchase.order}: | !python {model: purchase.order}: |
self.copy(cr, uid, ref('purchase_order_1'), context) self.copy_data(cr, uid, ref('purchase_order_1'), context)

View File

@ -28,6 +28,7 @@ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FO
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
from openerp import workflow from openerp import workflow
class sale_order(osv.osv): class sale_order(osv.osv):
_name = "sale.order" _name = "sale.order"
_inherit = ['mail.thread', 'ir.needaction_mixin'] _inherit = ['mail.thread', 'ir.needaction_mixin']
@ -676,6 +677,8 @@ class sale_order(osv.osv):
:return: True :return: True
""" """
if not context:
context = {}
procurement_obj = self.pool.get('procurement.order') procurement_obj = self.pool.get('procurement.order')
sale_line_obj = self.pool.get('sale.order.line') sale_line_obj = self.pool.get('sale.order.line')
for order in self.browse(cr, uid, ids, context=context): for order in self.browse(cr, uid, ids, context=context):
@ -701,7 +704,11 @@ class sale_order(osv.osv):
proc_ids.append(proc_id) proc_ids.append(proc_id)
#Confirm procurement order such that rules will be applied on it #Confirm procurement order such that rules will be applied on it
#note that the workflow normally ensure proc_ids isn't an empty list #note that the workflow normally ensure proc_ids isn't an empty list
procurement_obj.run(cr, uid, proc_ids, context=context) ctx = context.copy()
ctx["no_picking_assign"] = True
procurement_obj.run(cr, uid, proc_ids, context=ctx)
#Check all moves associated and do the picking_assign
procurement_obj.group_picking_assign(cr, uid, proc_ids, context=context)
#if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
if order.state == 'shipping_except': if order.state == 'shipping_except':
@ -1170,3 +1177,5 @@ class procurement_order(osv.osv):
'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'), 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
} }
def group_picking_assign(self, cr, uid, proc_ids, context=None):
return True

View File

@ -67,7 +67,7 @@ class sale_order(osv.osv):
def _get_orders_procurements(self, cr, uid, ids, context=None): def _get_orders_procurements(self, cr, uid, ids, context=None):
res = set() res = set()
for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context): for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context):
if proc.sale_line_id: if proc.state =='done' and proc.sale_line_id:
res.add(proc.sale_line_id.order_id.id) res.add(proc.sale_line_id.order_id.id)
return list(res) return list(res)
@ -102,7 +102,6 @@ class sale_order(osv.osv):
], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""), help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""),
'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={ 'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={
'stock.move': (_get_orders, ['state'], 10),
'procurement.order': (_get_orders_procurements, ['state'], 10) 'procurement.order': (_get_orders_procurements, ['state'], 10)
}), }),
'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True), 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True),
@ -454,3 +453,18 @@ class stock_picking(osv.osv):
created_lines = sale_line_obj.invoice_line_create(cr, uid, sale_line_ids, context=context) created_lines = sale_line_obj.invoice_line_create(cr, uid, sale_line_ids, context=context)
invoice_line_obj.write(cr, uid, created_lines, {'invoice_id': invoice_id}, context=context) invoice_line_obj.write(cr, uid, created_lines, {'invoice_id': invoice_id}, context=context)
return invoice_id return invoice_id
class procurement_order(osv.osv):
_inherit = 'procurement.order'
def group_picking_assign(self, cr, uid, proc_ids, context=None):
moves = []
procurements = proc_ids
while procurements:
related_moves = []
for proc in self.browse(cr, uid, procurements, context=context):
related_moves += proc.move_ids
procurements = self.search(cr, uid, [('move_dest_id', 'in', [x.id for x in related_moves])], context=context)
moves += related_moves
self.pool.get("stock.move")._group_picking_assign(cr, uid, moves, context=context)

View File

@ -30,9 +30,9 @@ from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FO
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
#---------------------------------------------------------- #----------------------------------------------------------
# Incoterms # Incoterms
#---------------------------------------------------------- #----------------------------------------------------------
@ -306,6 +306,7 @@ class stock_quant(osv.osv):
:param link: browse record (stock.move.operation.link) :param link: browse record (stock.move.operation.link)
''' '''
toreserve = [] toreserve = []
reserved_availability = move.reserved_availability
#split quants if needed #split quants if needed
for quant, qty in quants: for quant, qty in quants:
if qty <= 0.0 or (quant and quant.qty <= 0.0): if qty <= 0.0 or (quant and quant.qty <= 0.0):
@ -314,17 +315,19 @@ class stock_quant(osv.osv):
continue continue
self._quant_split(cr, uid, quant, qty, context=context) self._quant_split(cr, uid, quant, qty, context=context)
toreserve.append(quant.id) toreserve.append(quant.id)
reserved_availability += quant.qty
#reserve quants #reserve quants
if toreserve: if toreserve:
self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id, 'link_move_operation_id': link and link.id or False}, context=context) self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id, 'link_move_operation_id': link and link.id or False}, context=context)
#check if move'state needs to be set as 'assigned' #check if move'state needs to be set as 'assigned'
move.refresh() if reserved_availability == move.product_qty and move.state in ('confirmed', 'waiting'):
if move.reserved_availability == move.product_qty and move.state in ('confirmed', 'waiting'):
self.pool.get('stock.move').write(cr, uid, [move.id], {'state': 'assigned'}, context=context) self.pool.get('stock.move').write(cr, uid, [move.id], {'state': 'assigned'}, context=context)
elif reserved_availability > 0 and not move.partially_available:
self.pool.get('stock.move').write(cr, uid, [move.id], {'partially_available': True}, context=context)
def quants_move(self, cr, uid, quants, move, location_to, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, context=None): def quants_move(self, cr, uid, quants, move, location_to, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, context=None):
"""Moves all given stock.quant in the given destination location. """Moves all given stock.quant in the given destination location.
:param quants: list of tuple(browse record(stock.quant) or None, quantity to move) :param quants: list of tuple(browse record(stock.quant) or None, quantity to move)
:param move: browse record (stock.move) :param move: browse record (stock.move)
:param location_to: browse record (stock.location) depicting where the quants have to be moved :param location_to: browse record (stock.location) depicting where the quants have to be moved
@ -333,39 +336,41 @@ class stock_quant(osv.osv):
:param src_package_id: ID of the package that contains the quants to move :param src_package_id: ID of the package that contains the quants to move
:param dest_package_id: ID of the package that must be set on the moved quant :param dest_package_id: ID of the package that must be set on the moved quant
""" """
quants_reconcile = []
quants_move = []
self._check_location(cr, uid, location_to, context=context)
for quant, qty in quants: for quant, qty in quants:
if not quant: if not quant:
#If quant is None, we will create a quant to move (and potentially a negative counterpart too) #If quant is None, we will create a quant to move (and potentially a negative counterpart too)
quant = self._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location=location_to, context=context) quant = self._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location = location_to, context=context)
self.move_single_quant(cr, uid, quant, location_to, qty, move, context=context) else:
self._quant_reconcile_negative(cr, uid, quant, move, context=context) quants_move += [(quant, qty)]
quants_reconcile.append(quant)
if quants_move:
self.move_single_quant_tuples(cr, uid, quants_move, move, location_to, dest_package_id, context=context)
self._quants_reconcile_negative(cr, uid, quants_reconcile, move, context=context)
def move_single_quant(self, cr, uid, quant, location_to, qty, move, context=None):
'''Moves the given 'quant' in 'location_to' for the given 'qty', and logs the stock.move that triggered this move in the quant history.
If needed, the quant may be split if it's not totally moved.
:param quant: browse record (stock.quant) def move_single_quant_tuples(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
:param location_to: browse record (stock.location) whole_quants = []
:param qty: float for quant, qty in quants:
:param move: browse record (stock.move) new_quant = self._quant_split(cr, uid, quant, qty, context=context)
''' quant.refresh()
new_quant = self._quant_split(cr, uid, quant, qty, context=context) whole_quants.append(quant.id)
vals = { if whole_quants:
'location_id': location_to.id, vals = {'location_id': location_dest_id.id,
'history_ids': [(4, move.id)], 'history_ids': [(4, move.id)],
} 'package_id': dest_package_id}
#if the quant we are moving had been split and was inside a package, it means we unpacked it self.write(cr, SUPERUSER_ID, whole_quants, vals, context=context)
if new_quant and new_quant.package_id:
vals['package_id'] = False
self.write(cr, SUPERUSER_ID, [quant.id], vals, context=context)
quant.refresh()
return new_quant
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): 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):
''' This function tries to find quants in the given location for the given domain, by trying to first limit ''' This function tries to find quants in the given location for the given domain, by trying to first limit
the choice on the quants that match the prefered_domain as well. But if the qty requested is not reached the choice on the quants that match the prefered_domain as well. But if the qty requested is not reached
it tries to find the remaining quantity by using the fallback_domain. it tries to find the remaining quantity by using the fallback_domain.
''' '''
if location.usage in ['inventory', 'production', 'supplier']:
return [(None, qty)]
if prefered_domain and fallback_domain: if prefered_domain and fallback_domain:
if domain is None: if domain is None:
domain = [] domain = []
@ -470,14 +475,22 @@ class stock_quant(osv.osv):
path.append((4, move.id)) path.append((4, move.id))
self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context) self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
def _quants_reconcile_negative(self, cr, uid, quants, move, context=None):
if quants[0].location_id.usage != 'internal':
return False
quants_rec = self.search(cr, uid, [('product_id', '=', move.product_id.id), ('qty','<', 0)], limit=1, context=context)
if quants_rec:
for quant in quants:
quant.refresh()
self._quant_reconcile_negative(cr, uid, quant, move, context=context)
def _quant_reconcile_negative(self, cr, uid, quant, move, context=None): def _quant_reconcile_negative(self, cr, uid, quant, move, context=None):
""" """
When new quant arrive in a location, try to reconcile it with When new quant arrive in a location, try to reconcile it with
negative quants. If it's possible, apply the cost of the new negative quants. If it's possible, apply the cost of the new
quant to the conter-part of the negative quant. quant to the conter-part of the negative quant.
""" """
if quant.location_id.usage != 'internal':
return False
solving_quant = quant solving_quant = quant
dom = [('qty', '<', 0)] dom = [('qty', '<', 0)]
if quant.lot_id: if quant.lot_id:
@ -522,6 +535,8 @@ class stock_quant(osv.osv):
def quants_unreserve(self, cr, uid, move, context=None): def quants_unreserve(self, cr, uid, move, context=None):
related_quants = [x.id for x in move.reserved_quant_ids] related_quants = [x.id for x in move.reserved_quant_ids]
if related_quants and move.partially_available:
self.pool.get("stock.move").write(cr, uid, [move.id], {'partially_available': False}, context=context)
return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'link_move_operation_id': False}, context=context) return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'link_move_operation_id': False}, context=context)
def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None): def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
@ -537,8 +552,6 @@ class stock_quant(osv.osv):
else: else:
domain += [('company_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)] domain += [('company_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)]
#don't take into account location that are production, supplier or inventory #don't take into account location that are production, supplier or inventory
ignore_location_ids = self.pool.get('stock.location').search(cr, uid, [('usage', 'in', ('production', 'supplier', 'inventory'))], context=context)
domain.append(('location_id','not in',ignore_location_ids))
res = [] res = []
offset = 0 offset = 0
while quantity > 0: while quantity > 0:
@ -569,16 +582,11 @@ class stock_quant(osv.osv):
''' Return the company owning the location if any ''' ''' Return the company owning the location if any '''
return location and (location.usage == 'internal') and location.company_id or False return location and (location.usage == 'internal') and location.company_id or False
def _check_location(self, cr, uid, ids, context=None): def _check_location(self, cr, uid, location, context=None):
for record in self.browse(cr, uid, ids, context=context): if location.usage == 'view':
if record.location_id.usage == 'view': raise osv.except_osv(_('Error'), _('You cannot move to a location of type view %s.') % (location.name))
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))
return True return True
_constraints = [
(_check_location, 'You cannot move products to a location of the type view.', ['location_id'])
]
#---------------------------------------------------------- #----------------------------------------------------------
# Stock Picking # Stock Picking
@ -636,19 +644,21 @@ class stock_picking(osv.osv):
''' '''
res = {} res = {}
for pick in self.browse(cr, uid, ids, context=context): for pick in self.browse(cr, uid, ids, context=context):
if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]): cr.execute("select state, partially_available from stock_move where picking_id = %s", (pick.id,))
move_lines = cr.fetchall() #x[0] = state, x[1] partially available
if (not move_lines) or any([x[0] == 'draft' for x in move_lines]):
res[pick.id] = 'draft' res[pick.id] = 'draft'
continue continue
if all([x.state == 'cancel' for x in pick.move_lines]): if all([x[0] == 'cancel' for x in move_lines]):
res[pick.id] = 'cancel' res[pick.id] = 'cancel'
continue continue
if all([x.state in ('cancel', 'done') for x in pick.move_lines]): if all([x[0] in ('cancel', 'done') for x in move_lines]):
res[pick.id] = 'done' res[pick.id] = 'done'
continue continue
order = {'confirmed': 0, 'waiting': 1, 'assigned': 2} order = {'confirmed': 0, 'waiting': 1, 'assigned': 2}
order_inv = {0: 'confirmed', 1: 'waiting', 2: 'assigned'} order_inv = {0: 'confirmed', 1: 'waiting', 2: 'assigned'}
lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel', 'done')] lst = [order[x[0]] for x in move_lines if x[0] not in ('cancel', 'done')]
if pick.move_type == 'one': if pick.move_type == 'one':
res[pick.id] = order_inv[min(lst)] res[pick.id] = order_inv[min(lst)]
else: else:
@ -658,8 +668,8 @@ class stock_picking(osv.osv):
res[pick.id] = order_inv[max(lst)] res[pick.id] = order_inv[max(lst)]
if not all(x == 2 for x in lst): if not all(x == 2 for x in lst):
#if all moves aren't assigned, check if we have one product partially available #if all moves aren't assigned, check if we have one product partially available
for move in pick.move_lines: for move in move_lines:
if move.reserved_quant_ids: if move[1]:
res[pick.id] = 'partially_available' res[pick.id] = 'partially_available'
break break
return res return res
@ -671,13 +681,6 @@ class stock_picking(osv.osv):
res.add(move.picking_id.id) res.add(move.picking_id.id)
return list(res) return list(res)
def _get_pickings_from_quant(self, cr, uid, ids, context=None):
res = set()
for quant in self.browse(cr, uid, ids, context=context):
if quant.reservation_id and quant.reservation_id.picking_id:
res.add(quant.reservation_id.picking_id.id)
return list(res)
def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None): def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
res = {} res = {}
for pick in self.browse(cr, uid, ids, context=context): for pick in self.browse(cr, uid, ids, context=context):
@ -708,9 +711,8 @@ class stock_picking(osv.osv):
'note': fields.text('Notes', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}), 'note': fields.text('Notes', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
'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"), '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"),
'state': fields.function(_state_get, type="selection", store={ 'state': fields.function(_state_get, type="selection", store={
'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'move_lines'], 20), 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type'], 20),
'stock.move': (_get_pickings, ['state', 'picking_id'], 20), 'stock.move': (_get_pickings, ['state', 'picking_id', 'partially_available'], 20)}, selection=[
'stock.quant': (_get_pickings_from_quant, ['reservation_id'], 20)}, selection=[
('draft', 'Draft'), ('draft', 'Draft'),
('cancel', 'Cancelled'), ('cancel', 'Cancelled'),
('waiting', 'Waiting Another Operation'), ('waiting', 'Waiting Another Operation'),
@ -729,9 +731,9 @@ class stock_picking(osv.osv):
), ),
'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Priority', required=True), 'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Priority', required=True),
'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date, 'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
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'), store={'stock.move': (_get_pickings, ['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'),
'max_date': fields.function(get_min_max_date, multi="min_max_date", 'max_date': fields.function(get_min_max_date, multi="min_max_date",
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"), store={'stock.move': (_get_pickings, ['date_expected'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
'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'), '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'),
'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}), 'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}), 'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
@ -838,8 +840,6 @@ class stock_picking(osv.osv):
for pick in self.browse(cr, uid, ids, context=context): for pick in self.browse(cr, uid, ids, context=context):
move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']] move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context) self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
if pick.pack_operation_exist:
self.do_prepare_partial(cr, uid, [pick.id], context=None)
return True return True
def action_cancel(self, cr, uid, ids, context=None): def action_cancel(self, cr, uid, ids, context=None):
@ -912,18 +912,19 @@ class stock_picking(osv.osv):
self.action_assign(cr, uid, picking_ids, context=context) self.action_assign(cr, uid, picking_ids, context=context)
self.do_prepare_partial(cr, uid, picking_ids, context=context) self.do_prepare_partial(cr, uid, picking_ids, context=context)
def _picking_putaway_resolution(self, cr, uid, picking, product, putaway, context=None): def _picking_putaway_resolution(self, cr, uid, picking, product, putaway, context=None):
if putaway.method == 'fixed' and putaway.location_spec_id: if putaway.method == 'fixed' and putaway.location_spec_id:
return putaway.location_spec_id.id return putaway.location_spec_id.id
return False return False
def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None): 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 """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. to move and their suggested destination, and returns the list of matching packages.
""" """
# Try to find as much as possible top-level packages that can be moved # Try to find as much as possible top-level packages that can be moved
pack_obj = self.pool.get("stock.quant.package") pack_obj = self.pool.get("stock.quant.package")
quant_obj = self.pool.get("stock.quant")
top_lvl_packages = set() top_lvl_packages = set()
quants_to_compare = quants_suggested_locations.keys() 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])): for pack in list(set([x.package_id for x in quants_suggested_locations.keys() if x and x.package_id])):
@ -932,9 +933,9 @@ class stock_picking(osv.osv):
good_pack = False good_pack = False
pack_destination = False pack_destination = False
while loop: while loop:
pack_quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context) pack_quants = pack_obj.get_contents(cr, uid, test_pack, context=context)
all_in = True all_in = True
for quant in quant_obj.browse(cr, uid, pack_quants, context=context): for quant in pack_quants:
# If the quant is not in the quants to compare and not in the common location # If the quant is not in the quants to compare and not in the common location
if not quant in quants_to_compare: if not quant in quants_to_compare:
all_in = False all_in = False
@ -947,7 +948,7 @@ class stock_picking(osv.osv):
all_in = False all_in = False
break break
if all_in: if all_in:
good_pack = test_pack.id good_pack = test_pack
if test_pack.parent_id: if test_pack.parent_id:
test_pack = test_pack.parent_id test_pack = test_pack.parent_id
else: else:
@ -996,17 +997,17 @@ class stock_picking(osv.osv):
#find the packages we can movei as a whole #find the packages we can movei as a whole
top_lvl_packages = self._get_top_level_packages(cr, uid, quants_suggested_locations, context=context) 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 # and then create pack operations for the top-level packages found
for pack in pack_obj.browse(cr, uid, top_lvl_packages, context=context): for pack in top_lvl_packages:
pack_quants = pack_obj.get_content(cr, uid, [pack.id], context=context) pack_quants = pack_obj.get_contents(cr, uid, pack, context=context)
vals.append({ vals.append({
'picking_id': picking.id, 'picking_id': picking.id,
'package_id': pack.id, 'package_id': pack.id,
'product_qty': 1.0, 'product_qty': 1.0,
'location_id': pack.location_id.id, 'location_id': pack.location_id.id,
'location_dest_id': quants_suggested_locations[quant_obj.browse(cr, uid, pack_quants[0], context=context)], '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 #remove the quants inside the package so that they are excluded from the rest of the computation
for quant in quant_obj.browse(cr, uid, pack_quants, context=context): for quant in pack_quants:
del quants_suggested_locations[quant] del quants_suggested_locations[quant]
# Go through all remaining reserved quants and group by product, package, lot, owner, source location and dest location # Go through all remaining reserved quants and group by product, package, lot, owner, source location and dest location
@ -1043,35 +1044,38 @@ class stock_picking(osv.osv):
}) })
return vals return vals
def do_prepare_partial(self, cr, uid, picking_ids, context=None): def do_prepare_partial(self, cr, uid, picking_ids, context=None):
context = context or {} context = context or {}
ctx = context.copy()
ctx['no_recompute'] = True
pack_operation_obj = self.pool.get('stock.pack.operation') pack_operation_obj = self.pool.get('stock.pack.operation')
#get list of existing operations and delete them #get list of existing operations and delete them
existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context) existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context)
if existing_package_ids: if existing_package_ids:
pack_operation_obj.unlink(cr, uid, existing_package_ids, context) pack_operation_obj.unlink(cr, uid, existing_package_ids, context)
for picking in self.browse(cr, uid, picking_ids, context=context): for picking in self.browse(cr, uid, picking_ids, context=context):
forced_qties = {} # Quantity remaining after calculating reserved quants forced_qties = {} # Quantity remaining after calculating reserved quants
picking_quants = [] picking_quants = []
#Calculate packages, reserved quants, qtys of this picking's moves #Calculate packages, reserved quants, qtys of this picking's moves
for move in picking.move_lines: for move in picking.move_lines:
if move.state not in ('assigned', 'confirmed'): if move.state not in ('assigned', 'confirmed'):
continue continue
move_quants = move.reserved_quant_ids move_quants = move.reserved_quant_ids
picking_quants += move_quants picking_quants += move_quants
forced_qty = (move.state == 'assigned') and move.product_qty - move.reserved_availability or 0 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 we used force_assign() on the move, or if the move is incomming, forced_qty > 0
if forced_qty: if forced_qty:
if forced_qties.get(move.product_id): if forced_qties.get(move.product_id):
forced_qties[move.product_id] += forced_qty forced_qties[move.product_id] += forced_qty
else: else:
forced_qties[move.product_id] = forced_qty forced_qties[move.product_id] = forced_qty
for vals in self._prepare_pack_ops(cr, uid, picking, picking_quants, forced_qties, context=context): for vals in self._prepare_pack_ops(cr, uid, picking, picking_quants, forced_qties, context=context):
pack_operation_obj.create(cr, uid, vals, context=context) pack_operation_obj.create(cr, uid, vals, context=context)
def do_unreserve(self, cr, uid, picking_ids, context=None): def do_unreserve(self, cr, uid, picking_ids, context=None):
""" """
Will remove all quants for picking in picking_ids Will remove all quants for picking in picking_ids
@ -1086,12 +1090,127 @@ class stock_picking(osv.osv):
self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context) 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) 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_product(product_id, qty):
qty_to_assign = qty
if prod_move.get(product_id):
for move in prod_move[product_id]:
qty_on_link = min(qty_move_rem[move.id], qty_to_assign)
if qty_on_link > 0:
cr.execute("""insert into stock_move_operation_link (move_id, operation_id, qty) values
(%s, %s, %s)""", (move.id, op.id, qty_on_link,))
qty_move_rem[move.id] -= qty_on_link
qty_to_assign -= qty_on_link
if qty_to_assign <= 0:
break
return qty_to_assign == 0
def _check_quants_reserved(ops):
if ops.package_id and not ops.product_id:
qty_op_rem[ops.id] = {}
package_obj.get_contents(cr, uid, ops.package_id)
for quant in package_obj.get_contents(cr, uid, ops.package_id):
if quant.id in quants_done.keys() and (quants_done[quant.id] == quant.qty):
#Entire packages means entire quants from those packages
if not quants_done.get(quant.id):
quants_done[quant.id] = 0
cr.execute("""insert into stock_move_operation_link (move_id, operation_id, qty) values
(%s, %s, %s)""", (quant.reservation_id.id, ops.id, quant.qty,))
qty_move_rem[quant.reservation_id.id] -= quant.qty
else:
if qty_op_rem[ops.id].get(quant.product_id.id):
qty_op_rem[ops.id][quant.product_id.id] += quant.qty
else:
qty_op_rem[ops.id][quant.product_id.id] = quant.qty
else:
qty = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
#Check moves with same product
if prod_move.get(ops.product_id.id):
for move in prod_move[ops.product_id.id]:
for quant in move.reserved_quant_ids:
if not qty > 0:
break
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:
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:
quant_qty = quant.qty
if quants_done[quant.id] == 0:
continue
quant_qty = quants_done[quant.id]
if quant_qty > qty:
qty_todo = qty
quants_done[quant.id] = quant_qty - qty
else:
qty_todo = quant_qty
quants_done[quant.id] = 0
qty -= qty_todo
cr.execute("""insert into stock_move_operation_link (move_id, operation_id, qty) values
(%s, %s, %s)""", (move.id, ops.id, qty_todo,))
qty_move_rem[move.id] -= qty_todo
qty_op_rem[ops.id] = qty
uom_obj = self.pool.get('product.uom')
package_obj = self.pool.get('stock.quant.package')
quants_done = {}
prod_move = {}
qty_rem = {}
qty_move_rem = {}
qty_op_rem = {}
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))
cr.execute("""DELETE FROM stock_move_operation_link WHERE
operation_id in %s
""", (tuple([x.id for x in operations]),))
for move in picking.move_lines:
prod_qty = move.product_qty
qty_rem[move.id] = prod_qty
qty_move_rem[move.id] = prod_qty
#Fill moves by product dict
if not prod_move.get(move.product_id.id):
prod_move[move.product_id.id] = [move]
else:
prod_move[move.product_id.id].append(move)
# Fill qty remaining dict
for quant in move.reserved_quant_ids:
quants_done[quant.id] = quant.qty
sorted_moves = [x for x in picking.move_lines if x.state not in ['done', 'cancel']]
sorted_moves.sort(key=lambda x: qty_rem[x.id])
for op in operations:
_check_quants_reserved(op)
remaining_qty_ok = True
for op in operations:
op.refresh()
if op.product_id:
#TODO: Remaining qty: UoM conversions are done twice
normalized_qty = qty_op_rem[op.id]
if normalized_qty > 0:
remaining_qty_ok = remaining_qty_ok and _create_link_for_product(op.product_id.id, normalized_qty)
elif op.package_id:
for product_id, qty in qty_op_rem[op.id].items():
if qty > 0:
remaining_qty_ok = remaining_qty_ok and _create_link_for_product(product_id, qty)
quants_reserve_ok = all([quants_done[x] == 0 for x in quants_done.keys()])
return (quants_reserve_ok, remaining_qty_ok)
def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None): def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None):
pack_op_obj = self.pool.get('stock.pack.operation') quants_res = True
remaining_res = True
for picking in self.browse(cr, uid, picking_ids, context=context): for picking in self.browse(cr, uid, picking_ids, context=context):
op_ids = [op.id for op in picking.pack_operation_ids] if picking.pack_operation_ids:
if op_ids: (quants_ok, remaining_ok) = self.recompute_remaining_qty(cr, uid, picking, context=context)
pack_op_obj.recompute_rem_qty_from_operation(cr, uid, op_ids, context=context) quants_res = quants_res and quants_ok
remaining_res = remaining_res and remaining_ok
return (quants_res, remaining_res)
def _create_extra_moves(self, cr, uid, picking, context=None): 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 '''This function creates move lines on a picking, at the time of do_transfer, based on
@ -1126,6 +1245,7 @@ class stock_picking(osv.osv):
stock_move_obj.do_unreserve(cr, uid, move_ids, context=context) stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
stock_move_obj.action_assign(cr, uid, move_ids, context=context) stock_move_obj.action_assign(cr, uid, move_ids, context=context)
def do_transfer(self, cr, uid, picking_ids, context=None): def do_transfer(self, cr, uid, picking_ids, context=None):
""" """
If no pack operation, we do simple action_done of the picking If no pack operation, we do simple action_done of the picking
@ -1139,30 +1259,31 @@ class stock_picking(osv.osv):
self.action_done(cr, uid, [picking.id], context=context) self.action_done(cr, uid, [picking.id], context=context)
continue continue
else: else:
self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context) (quants_ok, remaining_ok) = self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
#create extra moves in the picking (unexpected product moves coming from pack operations) #create extra moves in the picking (unexpected product moves coming from pack operations)
self._create_extra_moves(cr, uid, picking, context=context) if not remaining_ok:
self._create_extra_moves(cr, uid, picking, context=context)
picking.refresh() picking.refresh()
#split move lines eventually #split move lines eventually
todo_move_ids = [] todo_move_ids = []
toassign_move_ids = [] toassign_move_ids = []
for move in picking.move_lines: for move in picking.move_lines:
remaining_qty = move.remaining_qty
if move.state in ('done', 'cancel'): if move.state in ('done', 'cancel'):
#ignore stock moves cancelled or already done #ignore stock moves cancelled or already done
continue continue
elif move.state == 'draft': elif move.state == 'draft':
toassign_move_ids.append(move.id) toassign_move_ids.append(move.id)
if move.remaining_qty == 0: if remaining_qty == 0:
if move.state in ('draft', 'assigned', 'confirmed'): if move.state in ('draft', 'assigned', 'confirmed'):
todo_move_ids.append(move.id) todo_move_ids.append(move.id)
elif move.remaining_qty > 0 and move.remaining_qty < move.product_qty: elif remaining_qty > 0 and remaining_qty < move.product_qty:
new_move = stock_move_obj.split(cr, uid, move, move.remaining_qty, context=context) new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context)
todo_move_ids.append(move.id) todo_move_ids.append(move.id)
#Assign move as it was assigned before #Assign move as it was assigned before
toassign_move_ids.append(new_move) toassign_move_ids.append(new_move)
else: if (not quants_ok or not remaining_ok) and not picking.location_id.usage in ("supplier", "production", "inventory"):
todo_move_ids.append(move.id) self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context)
self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context)
if todo_move_ids and not context.get('do_only_split'): 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) self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context)
elif context.get('do_only_split'): elif context.get('do_only_split'):
@ -1319,7 +1440,7 @@ class stock_move(osv.osv):
uom_obj = self.pool.get('product.uom') uom_obj = self.pool.get('product.uom')
res = {} res = {}
for m in self.browse(cr, uid, ids, context=context): for m in self.browse(cr, uid, ids, context=context):
res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False) res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False, context=context)
return res return res
def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None): def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
@ -1330,7 +1451,7 @@ class stock_move(osv.osv):
for record in move.linked_move_operation_ids: for record in move.linked_move_operation_ids:
qty -= record.qty qty -= record.qty
#converting the remaining quantity in the move UoM #converting the remaining quantity in the move UoM
res[move.id] = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id) res[move.id] = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom, round=False, context=context)
return res return res
def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None): def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
@ -1366,7 +1487,7 @@ class stock_move(osv.osv):
res[move.id] = '' # 'not applicable' or 'n/a' could work too res[move.id] = '' # 'not applicable' or 'n/a' could work too
continue continue
total_available = min(move.product_qty, move.reserved_availability + move.availability) total_available = min(move.product_qty, move.reserved_availability + move.availability)
total_available = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, total_available, move.product_uom.id) total_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, total_available, move.product_uom, context=context)
info = str(total_available) info = str(total_available)
#look in the settings if we need to display the UoM name or not #look in the settings if we need to display the UoM name or not
config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context) config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
@ -1377,7 +1498,7 @@ class stock_move(osv.osv):
if move.reserved_availability: if move.reserved_availability:
if move.reserved_availability != total_available: if move.reserved_availability != total_available:
#some of the available quantity is assigned and some are available but not reserved #some of the available quantity is assigned and some are available but not reserved
reserved_available = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, move.reserved_availability, move.product_uom.id) reserved_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, move.reserved_availability, move.product_uom, context=context)
info += _(' (%s reserved)') % str(reserved_available) info += _(' (%s reserved)') % str(reserved_available)
else: else:
#all available quantity is assigned #all available quantity is assigned
@ -1404,6 +1525,7 @@ class stock_move(osv.osv):
res += [x.id for x in picking.move_lines] res += [x.id for x in picking.move_lines]
return res return res
_columns = { _columns = {
'name': fields.char('Description', required=True, select=True), 'name': fields.char('Description', required=True, select=True),
'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'), 'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
@ -1412,7 +1534,7 @@ class stock_move(osv.osv):
'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"), 'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"),
'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}), 'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}),
# TODO: improve store to add dependency on product UoM # TODO: improve store to add dependency on product UoM
'product_qty': fields.function(_quantity_normalize, type='float', store=True, string='Quantity', 'product_qty': fields.function(_quantity_normalize, type='float', store={'stock.move': (lambda self, cr, uid, ids, ctx: ids, ['product_uom_qty', 'product_uom'], 20)}, string='Quantity',
digits_compute=dp.get_precision('Product Unit of Measure'), digits_compute=dp.get_precision('Product Unit of Measure'),
help='Quantity in the default UoM of the product'), help='Quantity in the default UoM of the product'),
'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
@ -1456,7 +1578,7 @@ class stock_move(osv.osv):
"* 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"\ "* 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"\
"* Available: When products are reserved, it is set to \'Available\'.\n"\ "* Available: When products are reserved, it is set to \'Available\'.\n"\
"* Done: When the shipment is processed, the state is \'Done\'."), "* Done: When the shipment is processed, the state is \'Done\'."),
'partially_available': fields.boolean('Partially Available', readonly = True, help = "Checks if the move has some stock reserved"),
'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 '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
'company_id': fields.many2one('res.company', 'Company', required=True, select=True), 'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
@ -1526,16 +1648,6 @@ class stock_move(osv.osv):
'propagate': True, 'propagate': True,
} }
def _check_uom(self, cr, uid, ids, context=None):
for move in self.browse(cr, uid, ids, context=context):
if move.product_id.uom_id.category_id.id != move.product_uom.category_id.id:
return False
return True
_constraints = [
(_check_uom,
'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.',
['product_uom'])]
def copy_data(self, cr, uid, id, default=None, context=None): def copy_data(self, cr, uid, id, default=None, context=None):
if default is None: if default is None:
@ -1606,6 +1718,7 @@ class stock_move(osv.osv):
push_obj._apply(cr, uid, rule, move, context=context) push_obj._apply(cr, uid, rule, move, context=context)
return True return True
def _create_procurement(self, cr, uid, move, context=None): def _create_procurement(self, cr, uid, move, context=None):
""" This will create a procurement order """ """ This will create a procurement order """
return self.pool.get("procurement.order").create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context)) return self.pool.get("procurement.order").create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
@ -1618,6 +1731,17 @@ class stock_move(osv.osv):
# Check that we do not modify a stock.move which is done # Check that we do not modify a stock.move which is done
frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id']) frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
for move in self.browse(cr, uid, ids, context=context): for move in self.browse(cr, uid, ids, context=context):
#Check UoM in meantime
if vals.get('product_uom') or vals.get('product_id'):
product_uom = move.product_id.uom_id
move_uom = move.product_uom
if vals.get('product_uom'):
move_uom = self.pool.get('product.uom').browse(cr, uid, vals['product_uom'], context=context)
if vals.get('product_id'):
product_uom = self.pool.get('product.product').browse(cr, uid, vals['product_id'], context=context).uom_id
if move_uom.category_id.id != product_uom.category_id.id:
raise osv.except_osv(_('Operation Forbidden'),
_('Category of Product UoM must be the same as the category of the UoM of the move'))
if move.state == 'done': if move.state == 'done':
if frozen_fields.intersection(vals): if frozen_fields.intersection(vals):
raise osv.except_osv(_('Operation Forbidden!'), raise osv.except_osv(_('Operation Forbidden!'),
@ -1767,6 +1891,10 @@ class stock_move(osv.osv):
return {'value': result} return {'value': result}
def _picking_assign(self, cr, uid, move, context=None): def _picking_assign(self, cr, uid, move, context=None):
if not context:
context = {}
if context.get("no_picking_assign") and context['no_picking_assign']:
return False
if move.picking_id or not move.picking_type_id: if move.picking_id or not move.picking_type_id:
return False return False
context = context or {} context = context or {}
@ -1792,6 +1920,40 @@ class stock_move(osv.osv):
move.write({'picking_id': pick}) move.write({'picking_id': pick})
return True return True
def _group_picking_assign(self, cr, uid, moves, context=None):
if not context:
context = {}
if context.get("no_picking_assign"):
return False
move_dict = {}
for move in moves:
group_by = (move.location_id, move.location_dest_id, move.group_id)
if not move_dict.get(group_by, False):
move_dict[group_by] = [move]
else:
move_dict[group_by].append(move)
pick_obj = self.pool.get("stock.picking")
for to_compare in move_dict.keys():
picks = pick_obj.search(cr, uid, [
('group_id', '=', to_compare[2].id),
('location_id', '=', to_compare[0].id),
('location_dest_id', '=', to_compare[1].id),
('state', 'in', ['draft', 'confirmed', 'waiting']),
], context=context)
if picks:
pick = picks[0]
else:
move = move_dict[to_compare][0]
values = {
'origin': move.origin,
'company_id': move.company_id and move.company_id.id or False,
'move_type': move.group_id and move.group_id.move_type or 'one',
'partner_id': move.group_id and move.group_id.partner_id and move.group_id.partner_id.id or False,
'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
}
pick = pick_obj.create(cr, uid, values, context=context)
self.write(cr, uid, [x.id for x in move_dict[to_compare]], {'picking_id': pick}, context=context)
def onchange_date(self, cr, uid, ids, date, date_expected, context=None): def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
""" On change of Scheduled Date gives a Move date. """ On change of Scheduled Date gives a Move date.
@param date_expected: Scheduled Date @param date_expected: Scheduled Date
@ -1909,11 +2071,12 @@ class stock_move(osv.osv):
#first try to find quants based on specific domains given by linked operations #first try to find quants based on specific domains given by linked operations
for record in ops.linked_move_operation_ids: for record in ops.linked_move_operation_ids:
move = record.move_id move = record.move_id
domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context) if move.id in main_domain:
qty_already_assigned = sum([q.qty for q in record.reserved_quant_ids]) domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
qty = record.qty - qty_already_assigned qty_already_assigned = sum([q.qty for q in record.reserved_quant_ids])
quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.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) qty = record.qty - qty_already_assigned
quant_obj.quants_reserve(cr, uid, quants, move, record, context=context) quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.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)
quant_obj.quants_reserve(cr, uid, quants, move, record, context=context)
for move in todo_moves: for move in todo_moves:
#then if the move isn't totally assigned, try to find quants without any specific domain #then if the move isn't totally assigned, try to find quants without any specific domain
@ -1951,11 +2114,11 @@ class stock_move(osv.osv):
self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}) self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'})
return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}) return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
def _check_package_from_moves(self, cr, uid, ids, context=None): def _check_package_from_moves(self, cr, uid, moves, context=None):
pack_obj = self.pool.get("stock.quant.package") pack_obj = self.pool.get("stock.quant.package")
packs = set() packs = set()
for move in self.browse(cr, uid, ids, context=context): for move in moves:
packs |= set([q.package_id.id for q in move.quant_ids if q.package_id and q.qty > 0]) 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) return pack_obj._check_location_constraint(cr, uid, list(packs), context=context)
def action_done(self, cr, uid, ids, context=None): def action_done(self, cr, uid, ids, context=None):
@ -1978,6 +2141,7 @@ class stock_move(osv.osv):
for link in move.linked_move_operation_ids: for link in move.linked_move_operation_ids:
operations.add(link.operation_id) operations.add(link.operation_id)
#Sort operations according to entire packages first, then package + lot, package only, lot only #Sort operations according to entire packages first, then package + lot, package only, lot only
operations = list(operations) 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)) 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))
@ -1995,13 +2159,15 @@ class stock_move(osv.osv):
quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain=prefered_domain, quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, 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) fallback_domain=fallback_domain, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
package_id = False package_id = False
if not record.operation_id.package_id: if not ops.product_id and ops.package_id:
#if a package and a result_package is given, we don't enter here because it will be processed by process_packaging() later #if a package and a result_package is given, we will put package_id, otherwise it will be result_pakege
#but for operations having only result_package_id, we will create new quants in the final package directly #but for operations having only result_package_id, we will create new quants in the final package directly
package_id = record.operation_id.result_package_id.id or False package_id = ops.package_id.id
else:
package_id = ops.result_package_id.id
quant_obj.quants_move(cr, uid, quants, move, ops.location_dest_id, 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) quant_obj.quants_move(cr, uid, quants, move, ops.location_dest_id, 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)
#packaging process # Handle pack in pack
pack_op_obj.process_packaging(cr, uid, ops, [x[0].id for x in quants if x[0]], context=context) pack_op_obj.process_packaging(cr, uid, ops, context=context)
move_qty[move.id] -= record.qty move_qty[move.id] -= record.qty
#Check for remaining qtys and unreserve/check move_dest_id in #Check for remaining qtys and unreserve/check move_dest_id in
for move in self.browse(cr, uid, ids, context=context): for move in self.browse(cr, uid, ids, context=context):
@ -2027,7 +2193,7 @@ class stock_move(osv.osv):
procurement_ids.append(move.procurement_id.id) procurement_ids.append(move.procurement_id.id)
# Check the packages have been placed in the correct locations # Check the packages have been placed in the correct locations
self._check_package_from_moves(cr, uid, ids, context=context) self._check_package_from_moves(cr, uid, self.browse(cr, uid, ids, context=context), context=context)
# Apply on picking # Apply on picking
self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) 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) self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
@ -2115,7 +2281,7 @@ class stock_move(osv.osv):
uom_obj = self.pool.get('product.uom') uom_obj = self.pool.get('product.uom')
context = context or {} context = context or {}
uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id) uom_qty = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom)
uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
defaults = { defaults = {
@ -3191,7 +3357,6 @@ class stock_location_path(osv.osv):
move_obj.action_confirm(cr, uid, [move_id], context=None) move_obj.action_confirm(cr, uid, [move_id], context=None)
# ------------------------- # -------------------------
# Packaging related stuff # Packaging related stuff
# ------------------------- # -------------------------
@ -3288,18 +3453,16 @@ class stock_package(osv.osv):
'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack') 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
} }
def _check_location_constraint(self, cr, uid, ids, context=None): def _check_location_constraint(self, cr, uid, packs, context=None):
'''checks that all quants in a package are stored in the same location. This function cannot be used '''checks that all quants in a package are stored in the same location. This function cannot be used
as a constraint because it needs to be checked on pack operations (they may not call write on the as a constraint because it needs to be checked on pack operations (they may not call write on the
package) package)
''' '''
quant_obj = self.pool.get('stock.quant') for pack in packs:
for pack in self.browse(cr, uid, ids, context=context):
parent = pack parent = pack
while parent.parent_id: while parent.parent_id:
parent = parent.parent_id parent = parent.parent_id
quant_ids = self.get_content(cr, uid, [parent.id], context=context) quants = self.get_contents(cr, uid, parent, context=context)
quants = quant_obj.browse(cr, uid, quant_ids, context=context)
location_id = quants and quants[0].location_id.id or False location_id = quants and quants[0].location_id.id or False
if not all([quant.location_id.id == location_id for quant in quants if quant.qty > 0]): if not all([quant.location_id.id == location_id for quant in quants if quant.qty > 0]):
raise osv.except_osv(_('Error'), _('Everything inside a package should be in the same location')) raise osv.except_osv(_('Error'), _('Everything inside a package should be in the same location'))
@ -3334,6 +3497,17 @@ class stock_package(osv.osv):
child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context) child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context) return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
def get_contents(self, cr, uid, pack, context=None):
quants = pack.quant_ids
children_ids = pack.children_ids
while children_ids:
children2_ids = []
for child in children_ids:
quants += child.quant_ids
children2_ids += child.children_ids
children_ids = children2_ids
return quants
def get_content_package(self, cr, uid, ids, context=None): def get_content_package(self, cr, uid, ids, context=None):
quants_ids = self.get_content(cr, uid, ids, context=context) quants_ids = self.get_content(cr, uid, ids, context=context)
res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context) res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
@ -3397,6 +3571,7 @@ class stock_pack_operation(osv.osv):
res[record.move_id.product_id.id] -= record.qty res[record.move_id.product_id.id] -= record.qty
return res return res
def _get_remaining_qty(self, cr, uid, ids, name, args, context=None): def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
uom_obj = self.pool.get('product.uom') uom_obj = self.pool.get('product.uom')
res = {} res = {}
@ -3409,12 +3584,12 @@ class stock_pack_operation(osv.osv):
else: else:
qty = ops.product_qty qty = ops.product_qty
if ops.product_uom_id: if ops.product_uom_id:
qty = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, ops.product_qty, ops.product_id.uom_id.id) qty = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
for record in ops.linked_move_operation_ids: for record in ops.linked_move_operation_ids:
qty -= record.qty qty -= record.qty
#converting the remaining quantity in the pack operation UoM #converting the remaining quantity in the pack operation UoM
if ops.product_uom_id: if ops.product_uom_id:
qty = uom_obj._compute_qty(cr, uid, ops.product_id.uom_id.id, qty, ops.product_uom_id.id) qty = uom_obj._compute_qty_obj(cr, uid, ops.product_id.uom_id, qty, ops.product_uom_id, context=context)
res[ops.id] = qty res[ops.id] = qty
return res return res
@ -3470,112 +3645,34 @@ class stock_pack_operation(osv.osv):
} }
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):
context = context or {}
res = super(stock_pack_operation, self).write(cr, uid, ids, vals, context=context) res = super(stock_pack_operation, self).write(cr, uid, ids, vals, context=context)
if isinstance(ids, (int, long)): if isinstance(ids, (int, long)):
ids = [ids] ids = [ids]
self.recompute_rem_qty_from_operation(cr, uid, ids, context=context) if not context.get("no_recompute"):
if vals.get('picking_id'):
self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, [vals['picking_id']], context=context)
else:
moves = self.browse(cr, uid, ids, context=context)
pickings = list(set([x.picking_id.id for x in moves]))
self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, pickings, context=context)
return res return res
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
context = context or {}
res_id = super(stock_pack_operation, self).create(cr, uid, vals, context=context) res_id = super(stock_pack_operation, self).create(cr, uid, vals, context=context)
self.recompute_rem_qty_from_operation(cr, uid, [res_id], context=context) if vals.get("picking_id") and not context.get("no_recompute"):
self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, [vals['picking_id']], context=context)
return res_id return res_id
def recompute_rem_qty_from_operation(self, cr, uid, op_ids, context=None):
def _create_link_for_product(product_id, qty):
qty_to_assign = qty
for move in sorted_moves:
if move.product_id.id == product_id and move.state not in ['done', 'cancel']:
qty_on_link = min(move.remaining_qty, qty_to_assign)
link_obj.create(cr, uid, {'move_id': move.id, 'operation_id': op.id, 'qty': qty_on_link}, context=context)
qty_to_assign -= qty_on_link
move.refresh()
if qty_to_assign <= 0:
break
def _check_quants_reserved(ops): def process_packaging(self, cr, uid, operation, context=None):
if ops.package_id and not ops.product_id:
for quant in quant_obj.browse(cr, uid, package_obj.get_content(cr, uid, [ops.package_id.id]), context=context):
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)):
#Entire packages means entire quants from those packages
if not quants_done.get(quant.id):
quants_done[quant.id] = 0
link_obj.create(cr, uid, {'move_id': quant.reservation_id.id, 'operation_id': ops.id, 'qty': quant.qty}, context=context)
else:
qty = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, ops.product_qty, ops.product_id.uom_id.id)
#Check moves with same product
for move in [x for x in ops.picking_id.move_lines if ops.product_id.id == x.product_id.id]:
for quant in move.reserved_quant_ids:
if not qty > 0:
break
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:
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:
quant_qty = quant.qty
if quants_done.get(quant.id):
if quants_done[quant.id] == 0:
continue
quant_qty = quants_done[quant.id]
if quant_qty > qty:
qty_todo = qty
quants_done[quant.id] = quant_qty - qty
else:
qty_todo = quant_qty
quants_done[quant.id] = 0
qty -= qty_todo
link_obj.create(cr, uid, {'move_id': quant.reservation_id.id, 'operation_id': ops.id, 'qty': qty_todo}, context=context)
link_obj = self.pool.get('stock.move.operation.link')
uom_obj = self.pool.get('product.uom')
package_obj = self.pool.get('stock.quant.package')
quant_obj = self.pool.get('stock.quant')
quants_done = {}
operations = self.browse(cr, uid, op_ids, context=context)
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))
sorted_moves = []
for op in operations:
if not sorted_moves:
#sort moves in order to process first the ones that have already reserved quants
sorted_moves = op.picking_id.move_lines
sorted_moves.sort(key=lambda x: x.product_qty - x.reserved_availability)
to_unlink_ids = [x.id for x in op.linked_move_operation_ids]
if to_unlink_ids:
link_obj.unlink(cr, uid, to_unlink_ids, context=context)
_check_quants_reserved(op)
for op in operations:
op.refresh()
if op.product_id:
#TODO: Remaining qty: UoM conversions are done twice
normalized_qty = uom_obj._compute_qty(cr, uid, op.product_uom_id.id, op.remaining_qty, op.product_id.uom_id.id)
if normalized_qty > 0:
_create_link_for_product(op.product_id.id, normalized_qty)
elif op.package_id:
prod_quants = self._get_remaining_prod_quantities(cr, uid, op, context=context)
for product_id, qty in prod_quants.items():
if qty > 0:
_create_link_for_product(product_id, qty)
def process_packaging(self, cr, uid, operation, quants, context=None):
''' Process the packaging of a given operation, after the quants have been moved. If there was not enough quants found ''' Process the packaging of a given operation, after the quants have been moved. If there was not enough quants found
a quant already has been with the good package information so we don't consider that case in this method''' a quant already has been with the good package information so we don't consider that case in this method'''
quant_obj = self.pool.get("stock.quant")
pack_obj = self.pool.get("stock.quant.package") pack_obj = self.pool.get("stock.quant.package")
for quant in quants: if not operation.product_id and operation.package_id and operation.result_package_id.id != operation.package_id.parent_id.id:
if quant: pack_obj.write(cr, SUPERUSER_ID, [operation.package_id.id], {'result_package_id': operation.result_package_id.id}, context=context)
if operation.product_id:
#if a product + a package information is given, we consider that we took a part of an existing package (unpacking)
quant_obj.write(cr, SUPERUSER_ID, quant, {'package_id': operation.result_package_id.id}, context=context)
elif operation.package_id and operation.result_package_id:
#move the whole pack into the final package if any
pack_obj.write(cr, uid, [operation.package_id.id], {'parent_id': operation.result_package_id.id}, context=context)

View File

@ -70,7 +70,7 @@ class stock_quant(osv.osv):
move = self._get_latest_move(cr, uid, quant, context=context) move = self._get_latest_move(cr, uid, quant, context=context)
# this is where we post accounting entries for adjustment # this is where we post accounting entries for adjustment
ctx['force_valuation_amount'] = newprice - quant.cost ctx['force_valuation_amount'] = newprice - quant.cost
self._account_entry_move(cr, uid, quant, move, context=ctx) self._account_entry_move(cr, uid, [quant], move, context=ctx)
#update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means #update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
#1) the product cost's method is 'real' #1) the product cost's method is 'real'
#2) we just fixed a negative quant caused by an outgoing shipment #2) we just fixed a negative quant caused by an outgoing shipment
@ -80,25 +80,27 @@ class stock_quant(osv.osv):
""" """
Accounting Valuation Entries Accounting Valuation Entries
location_from: can be None if it's a new quant quants: Quants to create accounting valuation entries for
move: Move to use
""" """
def _account_entry_move(self, cr, uid, quant, move, context=None): def _account_entry_move(self, cr, uid, quants, move, context=None):
location_from = move.location_id location_from = move.location_id
location_to = quant.location_id location_to = quants[0].location_id
if context is None: if context is None:
context = {} context = {}
if quant.product_id.valuation != 'real_time': if quants[0].product_id.valuation != 'real_time':
return False return False
if quant.owner_id: if quants[0].owner_id:
#if the quant isn't owned by the company, we don't make any valuation entry #if the quant isn't owned by the company, we don't make any valuation entry
return False return False
if quant.qty <= 0: if quants[0].qty <= 0:
#we don't make any stock valuation for negative quants because the valuation is already made for the counterpart. #we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
#At that time the valuation will be made at the product cost price and afterward there will be new accounting entries #At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
#to make the adjustments when we know the real cost price. #to make the adjustments when we know the real cost price.
return False return False
company_from = self._location_owner(cr, uid, quant, location_from, context=context)
company_to = self._location_owner(cr, uid, quant, location_to, context=context) company_from = self._location_owner(cr, uid, quants[0], location_from, context=context)
company_to = self._location_owner(cr, uid, quants[0], location_to, context=context)
if company_from == company_to: if company_from == company_to:
return False return False
@ -109,9 +111,9 @@ class stock_quant(osv.osv):
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx) journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
if location_from and location_from.usage == 'customer': if location_from and location_from.usage == 'customer':
#goods returned from customer #goods returned from customer
self._create_account_move_line(cr, uid, quant, move, acc_dest, acc_valuation, journal_id, context=ctx) self._create_account_move_line(cr, uid, quants, move, acc_dest, acc_valuation, journal_id, context=ctx)
else: else:
self._create_account_move_line(cr, uid, quant, move, acc_src, acc_valuation, journal_id, context=ctx) self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx)
# Create Journal Entry for products leaving the company # Create Journal Entry for products leaving the company
if company_from: if company_from:
@ -120,14 +122,22 @@ class stock_quant(osv.osv):
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx) journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
if location_to and location_to.usage == 'supplier': if location_to and location_to.usage == 'supplier':
#goods returned to supplier #goods returned to supplier
self._create_account_move_line(cr, uid, quant, move, acc_valuation, acc_src, journal_id, context=ctx) self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_src, journal_id, context=ctx)
else: else:
self._create_account_move_line(cr, uid, quant, move, acc_valuation, acc_dest, journal_id, context=ctx) self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
def move_single_quant(self, cr, uid, quant, location_to, qty, move, context=None): 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):
quant_record = super(stock_quant, self).move_single_quant(cr, uid, quant, location_to, qty, move, context=context) quant = super(stock_quant, self)._quant_create(cr, uid, qty, move, lot_id, owner_id, src_package_id, dest_package_id, force_location, context=context)
self._account_entry_move(cr, uid, quant, move, context=context) if move.product_id.valuation == 'real_time':
self._account_entry_move(cr, uid, [quant], move, context)
return quant
def move_single_quant_tuples(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
quant_record = super(stock_quant, self).move_single_quant_tuples(cr, uid, quants, move, location_dest_id, dest_package_id, context=context)
if move.product_id.valuation == 'real_time':
quants_filt = [x[0] for x in quants]
self._account_entry_move(cr, uid, quants_filt, move, context=context)
return quant_record return quant_record
@ -164,7 +174,7 @@ class stock_quant(osv.osv):
''') % (acc_src, acc_dest, acc_valuation, journal_id)) ''') % (acc_src, acc_dest, acc_valuation, journal_id))
return journal_id, acc_src, acc_dest, acc_valuation return journal_id, acc_src, acc_dest, acc_valuation
def _prepare_account_move_line(self, cr, uid, quant, move, credit_account_id, debit_account_id, context=None): def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None):
""" """
Generate the account.move.line values to post to track the stock valuation difference due to the Generate the account.move.line values to post to track the stock valuation difference due to the
processing of the given quant. processing of the given quant.
@ -175,16 +185,17 @@ class stock_quant(osv.osv):
if context.get('force_valuation_amount'): if context.get('force_valuation_amount'):
valuation_amount = context.get('force_valuation_amount') valuation_amount = context.get('force_valuation_amount')
else: else:
valuation_amount = quant.product_id.cost_method == 'real' and quant.cost or quant.product_id.standard_price valuation_amount = move.product_id.cost_method == 'real' and cost or move.product_id.standard_price
#the standard_price of the product may be in another decimal precision, or not compatible with the coinage of #the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
#the company currency... so we need to use round() before creating the accounting entries. #the company currency... so we need to use round() before creating the accounting entries.
valuation_amount = currency_obj.round(cr, uid, quant.company_id.currency_id, valuation_amount * quant.qty) valuation_amount = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty)
partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False 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 = { debit_line_vals = {
'name': move.name, 'name': move.name,
'product_id': quant.product_id.id, 'product_id': move.product_id.id,
'quantity': quant.qty, 'quantity': qty,
'product_uom_id': quant.product_id.uom_id.id, 'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False, 'ref': move.picking_id and move.picking_id.name or False,
'date': time.strftime('%Y-%m-%d'), 'date': time.strftime('%Y-%m-%d'),
'partner_id': partner_id, 'partner_id': partner_id,
@ -194,9 +205,9 @@ class stock_quant(osv.osv):
} }
credit_line_vals = { credit_line_vals = {
'name': move.name, 'name': move.name,
'product_id': quant.product_id.id, 'product_id': move.product_id.id,
'quantity': quant.qty, 'quantity': qty,
'product_uom_id': quant.product_id.uom_id.id, 'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False, 'ref': move.picking_id and move.picking_id.name or False,
'date': time.strftime('%Y-%m-%d'), 'date': time.strftime('%Y-%m-%d'),
'partner_id': partner_id, 'partner_id': partner_id,
@ -206,12 +217,22 @@ class stock_quant(osv.osv):
} }
return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)] return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
def _create_account_move_line(self, cr, uid, quant, move, credit_account_id, debit_account_id, journal_id, context=None): def _create_account_move_line(self, cr, uid, quants, move, credit_account_id, debit_account_id, journal_id, context=None):
#group quants by cost
quant_cost = {}
quant_cost_qty = {}
for quant in quants:
if quant_cost.get(quant.cost):
quant_cost_qty[quant.cost] += quant.qty
else:
quant_cost[quant.cost] = quant
quant_cost_qty[quant.cost] = quant.qty
move_obj = self.pool.get('account.move') move_obj = self.pool.get('account.move')
move_lines = self._prepare_account_move_line(cr, uid, quant, move, credit_account_id, debit_account_id, context=context) for cost in quant_cost_qty.keys():
return move_obj.create(cr, uid, {'journal_id': journal_id, move_lines = self._prepare_account_move_line(cr, uid, move, quant_cost_qty[cost], cost, credit_account_id, debit_account_id, context=context)
'line_id': move_lines, return move_obj.create(cr, uid, {'journal_id': journal_id,
'ref': move.picking_id and move.picking_id.name}, context=context) 'line_id': move_lines,
'ref': move.picking_id and move.picking_id.name}, context=context)
#def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None): #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
# move = self._get_latest_move(cr, uid, to_solve_quant, context=context) # move = self._get_latest_move(cr, uid, to_solve_quant, context=context)

View File

@ -56,6 +56,7 @@ class stock_history(osv.osv):
def _get_inventory_value(self, cr, uid, ids, name, attr, context=None): def _get_inventory_value(self, cr, uid, ids, name, attr, context=None):
product_obj = self.pool.get("product.product") product_obj = self.pool.get("product.product")
res = {} res = {}
#Browse takes an immense amount of time because it seems to reload the report
for line in self.browse(cr, uid, ids, context=context): for line in self.browse(cr, uid, ids, context=context):
if line.product_id.cost_method == 'real': if line.product_id.cost_method == 'real':
res[line.id] = line.quantity * line.price_unit_on_quant res[line.id] = line.quantity * line.price_unit_on_quant

View File

@ -73,6 +73,12 @@
I confirm the second purchase order I confirm the second purchase order
- -
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lifo2} !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lifo2}
-
The behavior of fifo/lifo is not garantee if the quants are created at the same second, so i just wait one second
-
!python {model: stock.picking}: |
import time
time.sleep(1)
- -
Process the reception of purchase order 2 Process the reception of purchase order 2
- -