diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 40a51df91d2..20526e36bcb 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -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.")) 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 = { 'type': 'email', diff --git a/addons/product/product.py b/addons/product/product.py index 9169ec99ead..1928f6cbcc8 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -807,7 +807,9 @@ class product_product(osv.osv): result = [] for product in self.browse(cr, SUPERUSER_ID, ids, context=context): - sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids) + sellers = [] + if partner_id: + sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids) if sellers: for s in sellers: mydict = { diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 0626f785241..c68c8f42755 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -155,12 +155,19 @@ class purchase_order(osv.osv): def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None): res = {} - for purchase_id in ids: - picking_ids = set() - move_ids = self.pool.get('stock.move').search(cr, uid, [('purchase_line_id.order_id', '=', purchase_id)], context=context) - for move in self.pool.get('stock.move').browse(cr, uid, move_ids, context=context): - picking_ids.add(move.picking_id.id) - res[purchase_id] = list(picking_ids) + query = """ + SELECT picking_id, po.id FROM stock_picking p, stock_move m, purchase_order_line pol, purchase_order po + WHERE po.id in %s and po.id = pol.order_id and pol.id = m.purchase_line_id and m.picking_id = p.id + GROUP BY picking_id, po.id + + """ + cr.execute(query, (tuple(ids), )) + picks = cr.fetchall() + for pick_id, po_id in picks: + if not res.get(po_id): + res[po_id] = [pick_id] + else: + res[po_id].append(pick_id) return res STATE_SELECTION = [ diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index 5989584793d..f14087723f5 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -35,7 +35,7 @@ class stock_move(osv.osv): ids = [ids] res = super(stock_move, self).write(cr, uid, ids, vals, context=context) from openerp import workflow - if 'state' in vals: + if vals.get('state') in ['done', 'cancel']: for move in self.browse(cr, uid, ids, context=context): if move.purchase_line_id and move.purchase_line_id.order_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): picking_ids = set() 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) 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?', help='Does the picking contains some moves related to a purchase order invoiceable on the reception?', 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), }), } diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index ed89fa49a92..dbe64465047 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -67,7 +67,7 @@ class sale_order(osv.osv): def _get_orders_procurements(self, cr, uid, ids, context=None): res = set() 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) 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)]}, 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={ - 'stock.move': (_get_orders, ['state'], 10), 'procurement.order': (_get_orders_procurements, ['state'], 10) }), 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True), diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py index d9e018f1cb0..0f704957045 100644 --- a/addons/stock/procurement.py +++ b/addons/stock/procurement.py @@ -215,11 +215,22 @@ class procurement_order(osv.osv): return False move_obj = self.pool.get('stock.move') move_dict = self._run_move_create(cr, uid, procurement, context=context) - move_id = move_obj.create(cr, uid, move_dict, context=context) + move_obj.create(cr, uid, move_dict, context=context) self.message_post(cr, uid, [procurement.id], body=_("Supply Move created"), context=context) - move_obj.action_confirm(cr, uid, [move_id], context=context) return True - return super(procurement_order, self)._run(cr, uid, procurement, context) + return super(procurement_order, self)._run(cr, uid, procurement, context=context) + + def run(self, cr, uid, ids, context=None): + res = super(procurement_order, self).run(cr, uid, ids, context=context) + #after all the procurements are run, check if some created a draft stock move that needs to be confirmed + #(we do that in batch because it fasts the picking assignation and the picking state computation) + move_to_confirm_ids = [] + for procurement in self.browse(cr, uid, ids, context=context): + if procurement.state == "running" and procurement.rule_id and procurement.rule_id.action == "move": + move_to_confirm_ids += [m.id for m in procurement.move_ids if m.state == 'draft'] + if move_to_confirm_ids: + self.pool.get('stock.move').action_confirm(cr, uid, move_to_confirm_ids, context=context) + return res def _check(self, cr, uid, procurement, context=None): ''' Implement the procurement checking for rules of type 'move'. The procurement will be satisfied only if all related diff --git a/addons/stock/stock.py b/addons/stock/stock.py index b0c19c1a25b..daaf8270958 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -30,9 +30,9 @@ from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FO from openerp import SUPERUSER_ID import openerp.addons.decimal_precision as dp import logging + + _logger = logging.getLogger(__name__) - - #---------------------------------------------------------- # Incoterms #---------------------------------------------------------- @@ -61,6 +61,10 @@ class stock_location(osv.osv): _order = 'parent_left' _rec_name = 'complete_name' + def _location_owner(self, cr, uid, location, context=None): + ''' Return the company owning the location if any ''' + return location and (location.usage == 'internal') and location.company_id or False + def _complete_name(self, cr, uid, ids, name, args, context=None): """ Forms complete name of location from parent location to child location. @return: Dictionary of values @@ -242,7 +246,6 @@ class stock_quant(osv.osv): 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant"), 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', store=True), 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for"), - 'link_move_operation_id': fields.many2one('stock.move.operation.link', 'Reserved for Link between Move and Pack Operation', help="Technical field decpicting for with tuple (move, operation) this quant is reserved for"), 'lot_id': fields.many2one('stock.production.lot', 'Lot'), 'cost': fields.float('Unit Cost'), 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant"), @@ -306,6 +309,7 @@ class stock_quant(osv.osv): :param link: browse record (stock.move.operation.link) ''' toreserve = [] + reserved_availability = move.reserved_availability #split quants if needed for quant, qty in quants: if qty <= 0.0 or (quant and quant.qty <= 0.0): @@ -314,17 +318,18 @@ class stock_quant(osv.osv): continue self._quant_split(cr, uid, quant, qty, context=context) toreserve.append(quant.id) + reserved_availability += quant.qty #reserve quants 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}, context=context) #check if move'state needs to be set as 'assigned' - move.refresh() - if move.reserved_availability == move.product_qty and move.state in ('confirmed', 'waiting'): + if 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) + 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): """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 move: browse record (stock.move) :param location_to: browse record (stock.location) depicting where the quants have to be moved @@ -333,39 +338,40 @@ class stock_quant(osv.osv): :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 """ + quants_reconcile = [] + to_move_quants = [] + self._check_location(cr, uid, location_to, context=context) for quant, qty in quants: if not quant: #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) - self.move_single_quant(cr, uid, quant, location_to, qty, move, context=context) - self._quant_reconcile_negative(cr, uid, quant, move, context=context) + else: + self._quant_split(cr, uid, quant, qty, context=context) + quant.refresh() + to_move_quants.append(quant) + quants_reconcile.append(quant) + if to_move_quants: + self.move_quants_write(cr, uid, to_move_quants, move, location_to, dest_package_id, context=context) + if location_to.usage == 'internal': + if self.search(cr, uid, [('product_id', '=', move.product_id.id), ('qty','<', 0)], limit=1, context=context): + for quant in quants_reconcile: + quant.refresh() + self._quant_reconcile_negative(cr, uid, quant, 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) - :param location_to: browse record (stock.location) - :param qty: float - :param move: browse record (stock.move) - ''' - new_quant = self._quant_split(cr, uid, quant, qty, context=context) - vals = { - 'location_id': location_to.id, - 'history_ids': [(4, move.id)], - } - #if the quant we are moving had been split and was inside a package, it means we unpacked it - 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 move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None): + vals = {'location_id': location_dest_id.id, + 'history_ids': [(4, move.id)], + 'package_id': dest_package_id} + self.write(cr, SUPERUSER_ID, [q.id for q in quants], vals, context=context) 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 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. ''' + #don't look for quants in location that are of type production, supplier or inventory. + if location.usage in ['inventory', 'production', 'supplier']: + return [(None, qty)] if prefered_domain and fallback_domain: if domain is None: domain = [] @@ -476,8 +482,6 @@ class stock_quant(osv.osv): negative quants. If it's possible, apply the cost of the new quant to the conter-part of the negative quant. """ - if quant.location_id.usage != 'internal': - return False solving_quant = quant dom = [('qty', '<', 0)] if quant.lot_id: @@ -522,7 +526,9 @@ class stock_quant(osv.osv): def quants_unreserve(self, cr, uid, move, context=None): related_quants = [x.id for x in move.reserved_quant_ids] - return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'link_move_operation_id': False}, context=context) + 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}, context=context) def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None): ''' Implementation of removal strategies @@ -536,9 +542,6 @@ class stock_quant(osv.osv): domain += [('company_id', '=', context.get('force_company'))] else: 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 - 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 = [] offset = 0 while quantity > 0: @@ -558,21 +561,16 @@ class stock_quant(osv.osv): return res def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None): - order = 'in_date' + order = 'in_date, id' return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) def _quants_get_lifo(self, cr, uid, location, product, quantity, domain=[], context=None): - order = 'in_date desc' + order = 'in_date desc, id desc' return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) - def _location_owner(self, cr, uid, quant, location, context=None): - ''' Return the company owning the location if any ''' - return location and (location.usage == 'internal') and location.company_id or False - - def _check_location(self, cr, uid, ids, context=None): - for record in self.browse(cr, uid, ids, context=context): - if record.location_id.usage == 'view': - 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)) + def _check_location(self, cr, uid, location, context=None): + if location.usage == 'view': + raise osv.except_osv(_('Error'), _('You cannot move to a location of type view %s.') % (location.name)) return True def _check_open_inventory_location(self, cr, uid, ids, context=None): @@ -597,7 +595,6 @@ class stock_quant(osv.osv): return True _constraints = [ - (_check_location, 'You cannot move products to a location of the type view.', ['location_id']), (_check_open_inventory_location, "A Physical Inventory is being conducted at this location", ['location_id']), ] @@ -606,7 +603,8 @@ class stock_quant(osv.osv): #check the inventory constraint before the write if isinstance(ids, (int, long)): ids = [ids] - self._check_open_inventory_location(cr, uid, ids, context=context) + if 'owner_id' in vals or 'lot_id' in vals or 'package_id' in vals or 'location_id' in vals or 'product_id' in vals: + self._check_open_inventory_location(cr, uid, ids, context=context) return super(stock_quant, self).write(cr, uid, ids, vals, context=context) @@ -689,7 +687,7 @@ class stock_picking(osv.osv): if not all(x == 2 for x in lst): #if all moves aren't assigned, check if we have one product partially available for move in pick.move_lines: - if move.reserved_quant_ids: + if move.partially_available: res[pick.id] = 'partially_available' break return res @@ -701,13 +699,6 @@ class stock_picking(osv.osv): res.add(move.picking_id.id) 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): res = {} for pick in self.browse(cr, uid, ids, context=context): @@ -738,9 +729,8 @@ class stock_picking(osv.osv): '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"), 'state': fields.function(_state_get, type="selection", store={ - 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'move_lines'], 20), - 'stock.move': (_get_pickings, ['state', 'picking_id'], 20), - 'stock.quant': (_get_pickings_from_quant, ['reservation_id'], 20)}, selection=[ + 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type'], 20), + 'stock.move': (_get_pickings, ['state', 'picking_id', 'partially_available'], 20)}, selection=[ ('draft', 'Draft'), ('cancel', 'Cancelled'), ('waiting', 'Waiting Another Operation'), @@ -759,9 +749,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), '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", - 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_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)]}), @@ -868,8 +858,6 @@ class stock_picking(osv.osv): 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']] 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 def action_cancel(self, cr, uid, ids, context=None): @@ -977,7 +965,7 @@ class stock_picking(osv.osv): all_in = False break if all_in: - good_pack = test_pack.id + good_pack = test_pack if test_pack.parent_id: test_pack = test_pack.parent_id else: @@ -1026,17 +1014,18 @@ class stock_picking(osv.osv): #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 pack_obj.browse(cr, uid, top_lvl_packages, context=context): - pack_quants = pack_obj.get_content(cr, uid, [pack.id], context=context) + for pack in top_lvl_packages: + pack_quants_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[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 - for quant in quant_obj.browse(cr, uid, pack_quants, context=context): + 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 @@ -1076,23 +1065,24 @@ class stock_picking(osv.osv): 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 move_quants = move.reserved_quant_ids 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 forced_qty: if forced_qties.get(move.product_id): @@ -1100,7 +1090,9 @@ class stock_picking(osv.osv): 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=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) def do_unreserve(self, cr, uid, picking_ids, context=None): """ @@ -1116,12 +1108,122 @@ class stock_picking(osv.osv): 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: + 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 + + uom_obj = self.pool.get('product.uom') + 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: + 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): - pack_op_obj = self.pool.get('stock.pack.operation') for picking in self.browse(cr, uid, picking_ids, context=context): - op_ids = [op.id for op in picking.pack_operation_ids] - if op_ids: - pack_op_obj.recompute_rem_qty_from_operation(cr, uid, op_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 @@ -1169,30 +1271,31 @@ class stock_picking(osv.osv): self.action_done(cr, uid, [picking.id], context=context) continue else: - self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context) + 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) - self._create_extra_moves(cr, uid, picking, context=context) + if not all_op_processed: + self._create_extra_moves(cr, uid, picking, context=context) picking.refresh() #split move lines eventually todo_move_ids = [] 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 move.remaining_qty == 0: + if remaining_qty == 0: if move.state in ('draft', 'assigned', 'confirmed'): todo_move_ids.append(move.id) - elif move.remaining_qty > 0 and move.remaining_qty < move.product_qty: - new_move = stock_move_obj.split(cr, uid, move, move.remaining_qty, context=context) + 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) - else: - todo_move_ids.append(move.id) - self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context) + 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) 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'): @@ -1349,7 +1452,7 @@ class stock_move(osv.osv): uom_obj = self.pool.get('product.uom') res = {} 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 def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None): @@ -1360,7 +1463,7 @@ class stock_move(osv.osv): for record in move.linked_move_operation_ids: qty -= record.qty #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 def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None): @@ -1396,7 +1499,7 @@ class stock_move(osv.osv): res[move.id] = '' # 'not applicable' or 'n/a' could work too continue 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) #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) @@ -1407,7 +1510,7 @@ class stock_move(osv.osv): if move.reserved_availability: if move.reserved_availability != total_available: #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) else: #all available quantity is assigned @@ -1442,7 +1545,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"), '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 - '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'), help='Quantity in the default UoM of the product'), 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), @@ -1486,7 +1589,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"\ "* Available: When products are reserved, it is set to \'Available\'.\n"\ "* 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 'company_id': fields.many2one('res.company', 'Company', required=True, select=True), @@ -1797,21 +1900,20 @@ class stock_move(osv.osv): result['location_dest_id'] = loc_dest_id return {'value': result} - def _picking_assign(self, cr, uid, move, context=None): - if move.picking_id or not move.picking_type_id: - return False - context = context or {} + def _picking_assign(self, cr, uid, move_ids, procurement_group, location_from, location_to, context=None): + """Assign a picking on the given move_ids, which is a list of move supposed to share the same procurement_group, location_from and location_to + (and company). Those attributes are also given as parameters. + """ pick_obj = self.pool.get("stock.picking") - picks = [] - group = move.group_id and move.group_id.id or False picks = pick_obj.search(cr, uid, [ - ('group_id', '=', group), - ('location_id', '=', move.location_id.id), - ('location_dest_id', '=', move.location_dest_id.id), + ('group_id', '=', procurement_group), + ('location_id', '=', location_from), + ('location_dest_id', '=', location_to), ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context) if picks: pick = picks[0] else: + move = self.browse(cr, uid, move_ids, context=context)[0] values = { 'origin': move.origin, 'company_id': move.company_id and move.company_id.id or False, @@ -1820,8 +1922,7 @@ class stock_move(osv.osv): 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False, } pick = pick_obj.create(cr, uid, values, context=context) - move.write({'picking_id': pick}) - return True + return self.write(cr, uid, move_ids, {'picking_id': pick}, context=context) def onchange_date(self, cr, uid, ids, date, date_expected, context=None): """ On change of Scheduled Date gives a Move date. @@ -1843,6 +1944,7 @@ class stock_move(osv.osv): 'confirmed': [], 'waiting': [] } + to_assign = {} for move in self.browse(cr, uid, ids, context=context): state = 'confirmed' #if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available) @@ -1855,9 +1957,13 @@ class stock_move(osv.osv): if move2.move_orig_ids: state = 'waiting' move2 = move2.split_from - states[state].append(move.id) - self._picking_assign(cr, uid, move, context=context) + + if not move.picking_id and move.picking_type_id: + key = (move.group_id.id, move.location_id.id, move.location_dest_id.id) + if key not in to_assign: + to_assign[key] = [] + to_assign[key].append(move.id) for move in self.browse(cr, uid, states['confirmed'], context=context): if move.procure_method == 'make_to_order': @@ -1868,6 +1974,10 @@ class stock_move(osv.osv): for state, write_ids in states.items(): if len(write_ids): self.write(cr, uid, write_ids, {'state': state}) + #assign picking in batch for all confirmed move that share the same details + for key, move_ids in to_assign.items(): + procurement_group, location_from, location_to = key + self._picking_assign(cr, uid, move_ids, procurement_group, location_from, location_to, context=context) moves = self.browse(cr, uid, ids, context=context) self._push_apply(cr, uid, moves, context=context) return ids @@ -1940,11 +2050,12 @@ class stock_move(osv.osv): #first try to find quants based on specific domains given by linked operations for record in ops.linked_move_operation_ids: move = record.move_id - domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context) - qty_already_assigned = sum([q.qty for q in record.reserved_quant_ids]) - qty = record.qty - qty_already_assigned - 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) + if move.id in main_domain: + domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context) + qty_already_assigned = record.reserved_quant_id and record.reserved_quant_id.qty or 0 + qty = record.qty - qty_already_assigned + 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: #then if the move isn't totally assigned, try to find quants without any specific domain @@ -2022,17 +2133,26 @@ class stock_move(osv.osv): prefered_domain = [('reservation_id', '=', move.id)] fallback_domain = [('reservation_id', '=', False)] self.check_tracking(cr, uid, move, ops.package_id.id or ops.lot_id.id, context=context) - dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context) - quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain=prefered_domain, + if record.reserved_quant_id: + quants = [(record.reserved_quant_id, record.qty)] + else: + dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context) + quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain=prefered_domain, 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 - if not record.operation_id.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 - #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 - 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 - pack_op_obj.process_packaging(cr, uid, ops, [x[0].id for x in quants if x[0]], context=context) + + if ops.result_package_id.id: + #if a result package is given, all quants go there + quant_dest_package_id = ops.result_package_id.id + elif ops.product_id and ops.package_id: + #if a package and a product is given, we will remove quants from the pack. + quant_dest_package_id = False + else: + #otherwise we keep the current pack of the quant, which may mean None + quant_dest_package_id = ops.package_id.id + quant_obj.quants_move(cr, uid, quants, move, ops.location_dest_id, lot_id=ops.lot_id.id, owner_id=ops.owner_id.id, src_package_id=ops.package_id.id, dest_package_id=quant_dest_package_id, context=context) + # Handle pack in pack + if not ops.product_id and ops.package_id and ops.result_package_id.id != ops.package_id.parent_id.id: + pack_obj.write(cr, SUPERUSER_ID, [ops.package_id.id], {'result_package_id': ops.result_package_id.id}, context=context) move_qty[move.id] -= record.qty #Check for remaining qtys and unreserve/check move_dest_id in for move in self.browse(cr, uid, ids, context=context): @@ -2147,7 +2267,7 @@ class stock_move(osv.osv): uom_obj = self.pool.get('product.uom') 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 defaults = { @@ -3239,7 +3359,6 @@ class stock_location_path(osv.osv): move_obj.action_confirm(cr, uid, [move_id], context=None) - # ------------------------- # Packaging related stuff # ------------------------- @@ -3457,12 +3576,12 @@ class stock_pack_operation(osv.osv): else: qty = ops.product_qty 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: qty -= record.qty #converting the remaining quantity in the pack operation UoM 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 return res @@ -3518,115 +3637,22 @@ class stock_pack_operation(osv.osv): } 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) if isinstance(ids, (int, long)): ids = [ids] - self.recompute_rem_qty_from_operation(cr, uid, ids, context=context) + if not context.get("no_recompute"): + pickings = vals.get('picking_id') and [vals['picking_id']] or list(set([x.picking_id.id for x in self.browse(cr, uid, ids, context=context)])) + self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, pickings, context=context) return res def create(self, cr, uid, vals, context=None): + context = context or {} 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 - 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): - 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 - 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") - for quant in quants: - if quant: - 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) - - - - #TODO: this function can be refactored def _search_and_increment(self, cr, uid, picking_id, domain, context=None): '''Search for an operation with given 'domain' in a picking, if it exists increment the qty (+1) otherwise create it @@ -3681,7 +3707,7 @@ class stock_move_operation_link(osv.osv): 'qty': fields.float('Quantity', help="Quantity of products to consider when talking about the contribution of this pack operation towards the remaining quantity of the move (and inverse). Given in the product main uom."), 'operation_id': fields.many2one('stock.pack.operation', 'Operation', required=True, ondelete="cascade"), 'move_id': fields.many2one('stock.move', 'Move', required=True, ondelete="cascade"), - 'reserved_quant_ids': fields.one2many('stock.quant', 'link_move_operation_id', 'Reserved quants'), + 'reserved_quant_id': fields.many2one('stock.quant', 'Reserved Quant', help="Technical field containing the quant that created this link between an operation and a stock move. Used at the stock_move_obj.action_done() time to avoid seeking a matching quant again"), } def get_specific_domain(self, cr, uid, record, context=None): diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index d1b23058406..9bbad8e52b4 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -70,38 +70,42 @@ class stock_quant(osv.osv): move = self._get_latest_move(cr, uid, quant, context=context) # this is where we post accounting entries for adjustment 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 #1) the product cost's method is 'real' #2) we just fixed a negative quant caused by an outgoing shipment if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal': self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context) - """ - Accounting Valuation Entries + def _account_entry_move(self, cr, uid, quants, move, context=None): + """ + Accounting Valuation Entries - location_from: can be None if it's a new quant - """ - def _account_entry_move(self, cr, uid, quant, move, context=None): - location_from = move.location_id - location_to = quant.location_id + quants: browse record list of Quants to create accounting valuation entries for. Unempty and all quants are supposed to have the same location id (thay already moved in) + move: Move to use. browse record + """ if context is None: context = {} - if quant.product_id.valuation != 'real_time': - return False - if quant.owner_id: - #if the quant isn't owned by the company, we don't make any valuation entry - return False - if quant.qty <= 0: - #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 - #to make the adjustments when we know the real cost price. - 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) + location_obj = self.pool.get('stock.location') + location_from = move.location_id + location_to = quants[0].location_id + company_from = location_obj._location_owner(cr, uid, location_from, context=context) + company_to = location_obj._location_owner(cr, uid, location_to, context=context) if company_from == company_to: return False + if move.product_id.valuation != 'real_time': + return False + for q in quants: + if q.owner_id: + #if the quant isn't owned by the company, we don't make any valuation entry + return False + if q.qty <= 0: + #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 + #to make the adjustments when we know the real cost price. + return False + # Create Journal Entry for products arriving in the company if company_to: ctx = context.copy() @@ -109,9 +113,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) if location_from and location_from.usage == '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: - 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 if company_from: @@ -120,15 +124,21 @@ class stock_quant(osv.osv): 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': #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: - 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 _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 = super(stock_quant, self)._quant_create(cr, uid, qty, move, lot_id, owner_id, src_package_id, dest_package_id, force_location, context=context) + if move.product_id.valuation == 'real_time': + self._account_entry_move(cr, uid, [quant], move, context) + return quant - def move_single_quant(self, cr, uid, quant, location_to, qty, move, context=None): - quant_record = super(stock_quant, self).move_single_quant(cr, uid, quant, location_to, qty, move, context=context) - self._account_entry_move(cr, uid, quant, move, context=context) - return quant_record + def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None): + res = super(stock_quant, self).move_quants_write(cr, uid, quants, move, location_dest_id, dest_package_id, context=context) + if move.product_id.valuation == 'real_time': + self._account_entry_move(cr, uid, quants, move, context=context) + return res def _get_accounting_data_for_valuation(self, cr, uid, move, context=None): @@ -164,7 +174,7 @@ class stock_quant(osv.osv): ''') % (acc_src, acc_dest, acc_valuation, journal_id)) 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 processing of the given quant. @@ -175,16 +185,16 @@ class stock_quant(osv.osv): if context.get('force_valuation_amount'): valuation_amount = context.get('force_valuation_amount') 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 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 debit_line_vals = { 'name': move.name, - 'product_id': quant.product_id.id, - 'quantity': quant.qty, - 'product_uom_id': quant.product_id.uom_id.id, + 'product_id': move.product_id.id, + 'quantity': qty, + 'product_uom_id': move.product_id.uom_id.id, 'ref': move.picking_id and move.picking_id.name or False, 'date': move.date, 'partner_id': partner_id, @@ -194,9 +204,9 @@ class stock_quant(osv.osv): } credit_line_vals = { 'name': move.name, - 'product_id': quant.product_id.id, - 'quantity': quant.qty, - 'product_uom_id': quant.product_id.uom_id.id, + 'product_id': move.product_id.id, + 'quantity': qty, + 'product_uom_id': move.product_id.uom_id.id, 'ref': move.picking_id and move.picking_id.name or False, 'date': move.date, 'partner_id': partner_id, @@ -206,13 +216,22 @@ class stock_quant(osv.osv): } 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_qty = {} + for quant in quants: + if quant_cost_qty.get(quant.cost): + quant_cost_qty[quant.cost] += quant.qty + else: + quant_cost_qty[quant.cost] = quant.qty 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) - return move_obj.create(cr, uid, {'journal_id': journal_id, 'period_id': self.pool.get('account.period').find(cr, uid, move.date, context=context)[0], - 'date': move.date, - 'line_id': move_lines, - 'ref': move.picking_id and move.picking_id.name}, context=context) + for cost, qty in quant_cost_qty.items(): + move_lines = self._prepare_account_move_line(cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=context) + return move_obj.create(cr, uid, {'journal_id': journal_id, + 'line_id': move_lines, + 'period_id': self.pool.get('account.period').find(cr, uid, move.date, context=context)[0], + 'date': move.date, + '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): # move = self._get_latest_move(cr, uid, to_solve_quant, context=context) diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py index 7657ac53b4b..5856d892bf8 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/wizard/stock_valuation_history.py @@ -56,6 +56,7 @@ class stock_history(osv.osv): def _get_inventory_value(self, cr, uid, ids, name, attr, context=None): product_obj = self.pool.get("product.product") 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): if line.product_id.cost_method == 'real': res[line.id] = line.quantity * line.price_unit_on_quant diff --git a/addons/stock_dropshipping/test/lifo_price.yml b/addons/stock_dropshipping/test/lifo_price.yml index 274c31e24f2..b54e6529405 100644 --- a/addons/stock_dropshipping/test/lifo_price.yml +++ b/addons/stock_dropshipping/test/lifo_price.yml @@ -73,7 +73,7 @@ I confirm the second purchase order - !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lifo2} -- +- Process the reception of purchase order 2 - !python {model: stock.picking}: |