From 42e0f4e659bceb1816fed9cc8b507eac288da045 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Wed, 17 Jul 2013 15:26:27 +0200 Subject: [PATCH] [WIP] negative quants handling bzr revid: qdp-launchpad@openerp.com-20130717132627-4cbei6m63lq9rr5p --- addons/stock/stock.py | 135 ++++++++++--------- addons/stock/wizard/stock_partial_picking.py | 2 +- addons/stock_account/stock_account.py | 13 +- 3 files changed, 77 insertions(+), 73 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index be38513c5ba..98ce9c59ee8 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -30,6 +30,7 @@ from openerp.tools.translate import _ from openerp import netsvc from openerp import tools from openerp.tools import float_compare, DEFAULT_SERVER_DATETIME_FORMAT +from openerp import SUPERUSER_ID import openerp.addons.decimal_precision as dp import logging _logger = logging.getLogger(__name__) @@ -191,21 +192,20 @@ class stock_quant(osv.osv): self.move_single_quant(cr, uid, quant, qty, move, context=context) def move_single_quant(self, cr, uid, quant, qty, move, context=None): - location_from = quant and quant.location_id if not quant: quant = self._quant_create(cr, uid, qty, move, context=context) else: self._quant_split(cr, uid, quant, qty, context=context) - self._quant_reconcile_negative(cr, uid, quant, context=context) - # FP Note: improve this using preferred locations location_to = move.location_dest_id self.write(cr, uid, [quant.id], { 'location_id': location_to.id, - 'reservation_id': move.move_dest_id and move.move_dest_id.id or False, + 'reservation_id': move.move_dest_id and move.move_dest_id.id or False, 'history_ids': [(4, move.id)] }) + quant.refresh() + self._quant_reconcile_negative(cr, uid, quant, context=context) return quant @@ -221,20 +221,17 @@ class stock_quant(osv.osv): :qty in UoM of product :lot_id NOT USED YET ! """ - - result= [] - if domain is None: - domain = [] - + result = [] domain = domain or [('qty','>',0.0)] - if location and qty>0: + if location: removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) or 'fifo' if removal_strategy=='fifo': result += self._quants_get_fifo(cr, uid, location, product, qty, domain, prefered_order=prefered_order, context=context) elif removal_strategy=='lifo': result += self._quants_get_lifo(cr, uid, location, product, qty, domain, prefered_order=prefered_order, context=context) else: - raise osv.except_osv(_('Error!'),_('Removal strategy %s not implemented.' % (removal_strategy,))) + raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,))) + return result @@ -244,71 +241,79 @@ class stock_quant(osv.osv): # Reconcile a positive quant with a negative is possible # def _quant_create(self, cr, uid, qty, move, context=None): - vals = { - 'product_id': move.product_id.id, - 'location_id': move.location_dest_id.id, - 'qty': qty, - 'history_ids': [(4, move.id)], - 'in_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'company_id': move.company_id.id, - } - quant_id = self.create(cr, uid, vals, context=context) - if move.location_id.usage == 'internal': - vals['location_id'] = move.location_id.id - vals['qty'] = -qty - vals['cost'] = 0.0 - new_quant_id = self.create(cr, uid, vals, context=context) - self.write(cr, uid, [quant_id], {'propagated_from_id': new_quant_id}, context=context) - obj = self.browse(cr, uid, quant_id, context=context) - # FP Note: TODO: compute the right price according to the move, with currency convert # QTY is normally already converted to main product's UoM price_unit = move.price_unit + vals = { + 'product_id': move.product_id.id, + 'location_id': move.location_dest_id.id, + 'qty': qty, + 'cost': price_unit, + 'history_ids': [(4, move.id)], + 'in_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'company_id': move.company_id.id, + } - self._price_update(cr, uid, obj, price_unit, context=context) - return obj + negative_quant_id = False + if move.location_id.usage == 'internal': + #if we were trying to move something from an internal location and reach here (quant creation), + #it means that a negative quant has to be created as well. + negative_vals = vals.copy() + negative_vals['location_id'] = move.location_id.id + negative_vals['qty'] = -qty + negative_vals['cost'] = price_unit + negative_quant_id = self.create(cr, uid, negative_vals, context=context) + + #create the quant + vals.update({'propagated_from_id': negative_quant_id}) + quant_id = self.create(cr, uid, vals, context=context) + return self.browse(cr, uid, quant_id, context=context) def _quant_split(self, cr, uid, quant, qty, context=None): - context=context or {} - if quant.qty<=qty: + context = context or {} + if (quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty): return False - new_quant = self.copy(cr, uid, quant.id, default={'qty': quant.qty-qty}, context=context) + new_quant = self.copy(cr, uid, quant.id, default={'qty': quant.qty - qty}, context=context) self.write(cr, uid, quant.id, {'qty': qty}, context=context) quant.refresh() - return new_quant + return self.browse(cr, uid, new_quant, context=context) + + def _get_latest_move(self, cr, uid, quant, context=None): + move = False + for m in quant.history_ids: + if not move or m.date > move.date: + move = m + return move - """ - When new quant arrive in a location, try to reconcile it with - negative quants. If it's possible, apply the cost of the new - quant to the conter-part of the negative quant. - """ def _quant_reconcile_negative(self, cr, uid, quant, context=None): - if quant.location_id.usage <> 'internal': return False - quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, [('qty','<','0')], context=context) + """ + When new quant arrive in a location, try to reconcile it with + 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 + quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, [('qty', '<', '0')], context=context) result = False for quant_neg, qty in quants: - if not quant_neg: continue - result=True - - self._quant_split(cr, uid, quant_neg, qty, context=context) + if not quant_neg: + continue + result = True + to_solve_quant = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context) + if not to_solve_quant: + continue + to_solve_quant = self.browse(cr, uid, to_solve_quant[0], context=context) + move = self._get_latest_move(cr, uid, to_solve_quant, context=context) self._quant_split(cr, uid, quant, qty, context=context) - - cost = quant.id - - self.write(cr, uid, [quant.id, quant_neg.id], { - 'cost': 0.0, - }, context=context) - - #TODO: In case of negative quants no removal strategy is applied -> actually removal strategy should be reversed? OR just by in_date? - quants2 = self._quants_get_order(cr, uid, False, quant.product_id, -quant_neg.qty, domain=[('propagated_from_id','=',quant_neg.id)], orderby='in_date', context=None) - for qu2, qt2 in quants2: - #TODO history ids on quant! - if not qu2: raise 'Error: negative stock linked to nothing' - self._quant_split(cr, uid, qu2, qt2, context=context) - self.write(cr, uid, [qu2.id], { - 'propagated_from_id': False - }, context=context) - self._price_update(cr, uid, qu2, cost, context=context) + remaining_to_solve_quant = self._quant_split(cr, uid, to_solve_quant, qty, context=context) + remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context) + #if the reconciliation was not complete, we need to link together the remaining parts + if remaining_to_solve_quant and remaining_neg_quant: + self.write(cr, uid, remaining_to_solve_quant.id, {'propagated_from_id': remaining_neg_quant.id}, context=context) + #delete the reconciled quants, as it is replaced by the solving quant + self.unlink(cr, SUPERUSER_ID, [quant_neg.id, to_solve_quant.id], context=context) + #call move_single_quant to ensure recursivity if necessary and do the stock valuation + self.move_single_quant(cr, uid, quant, qty, move, context=context) return result def _price_update(self, cr, uid, quant, newprice, context=None): @@ -351,15 +356,13 @@ class stock_quant(osv.osv): order = 'in_date' if prefered_order: order = prefered_order + ', in_date' - return self._quants_get_order(cr, uid, location, product, quantity, - domain, order, context=context) + 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=[], prefered_order=False, context=None): order = 'in_date desc' if prefered_order: order = prefered_order + ', in_date desc' - return self._quants_get_order(cr, uid, location, product, quantity, - domain, order, context=context) + return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) # Return the company owning the location if any def _location_owner(self, cr, uid, quant, location, context=None): diff --git a/addons/stock/wizard/stock_partial_picking.py b/addons/stock/wizard/stock_partial_picking.py index a2116358f0a..6126cd4014e 100644 --- a/addons/stock/wizard/stock_partial_picking.py +++ b/addons/stock/wizard/stock_partial_picking.py @@ -127,7 +127,7 @@ class stock_partial_picking(osv.osv_memory): def _partial_move_for(self, cr, uid, move): partial_move = { 'product_id': move.product_id.id, - 'quantity': move.product_qty - move.remaining_qty if move.state == 'assigned' else 0, + 'quantity': move.product_uom_qty, 'product_uom': move.product_uom.id, 'move_id': move.id, 'location_id': move.location_id.id, diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 6dcea7e8304..5186252d900 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -75,7 +75,11 @@ class stock_quant(osv.osv): def _account_entry_move(self, cr, uid, quant, location_from, location_to, move, context=None): if context is None: context = {} - if quant.product_id.valuation <> 'real_time': + if quant.product_id.valuation != 'real_time': + return False + if quant.qty <= 0 or quant.propagated_from_id: + #we don't make any stock valuation for negative quants because we may not know the real cost price. + #The valuation will be made at the time of the reconciliation of the negative quant. 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) @@ -112,9 +116,6 @@ class stock_quant(osv.osv): self._account_entry_move(cr, uid, quant, location_from, quant.location_id, move, context=context) return quant - - # TODO: move this code on the _account_entry_move method above. Should be simpler - def _get_accounting_data_for_valuation(self, cr, uid, move, context=None): """ Return the accounts and journal to use to post Journal Entries for the real-time @@ -124,7 +125,7 @@ class stock_quant(osv.osv): :returns: journal_id, source account, destination account, valuation account :raise: osv.except_osv() is any mandatory account or journal is not defined. """ - product_obj=self.pool.get('product.product') + product_obj = self.pool.get('product.product') accounts = product_obj.get_product_accounts(cr, uid, move.product_id.id, context) if move.location_id.valuation_out_account_id: acc_src = move.location_id.valuation_out_account_id.id @@ -140,7 +141,7 @@ class stock_quant(osv.osv): journal_id = accounts['stock_journal'] if not all([acc_src, acc_dest, acc_valuation, journal_id]): - raise osv.except_osv(_('Error!'), _('''One of the following information is missing on the product or product category and prevents the accounting valuation entries to be created: + raise osv.except_osv(_('Error!'), _('''One of the following information is missing on the product or product category and prevents the accounting valuation entries to be created: Stock Input Account: %s Stock Output Account: %s Stock Valuation Account: %s