diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index dbbd2f482d3..de92d48fb9e 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -119,6 +119,8 @@ class procurement_order(osv.osv): ('running', 'Running'), ('done', 'Done') ], 'Status', required=True, track_visibility='onchange'), + 'message': fields.text('Latest error', help="Exception occurred while computing procurement orders."), + } _defaults = { diff --git a/addons/sale/sale.py b/addons/sale/sale.py index f99b19b5d55..1e2ae1086cf 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -684,7 +684,7 @@ class sale_order(osv.osv): val['shipped'] = False if (order.order_policy == 'manual'): - for line in order.order_line: + for line in order.order_line: if (not line.invoiced) and (line.state not in ('cancel', 'draft')): val['state'] = 'manual' break diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py index 3e59c971d72..b909fe5ecd4 100644 --- a/addons/stock/procurement.py +++ b/addons/stock/procurement.py @@ -21,6 +21,7 @@ from openerp.osv import fields, osv from openerp.tools.translate import _ +import openerp class procurement_group(osv.osv): _inherit = 'procurement.group' @@ -86,7 +87,6 @@ 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.action_confirm(cr, uid, [move_id], context=context) self.write(cr, uid, [procurement.id], {'move_id': move_id}, context=context) @@ -98,4 +98,44 @@ class procurement_order(osv.osv): return procurement.move_id.state == 'done' return super(procurement_order, self)._check(cr, uid, procurement, context) + + + # + # Scheduler + # When stock is installed, it should also check for the different + # + # + def run_scheduler(self, cr, uid, use_new_cursor=False, context=None): + ''' + Call the scheduler in order to + + @param self: The object pointer + @param cr: The current row, from the database cursor, + @param uid: The current user ID for security checks + @param ids: List of selected IDs + @param use_new_cursor: False or the dbname + @param context: A standard dictionary for contextual values + @return: Dictionary of values + ''' + super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, context=context) + if context is None: + context = {} + try: + if use_new_cursor: + cr = openerp.registry(use_new_cursor).db.cursor() + + company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id + move_obj = self.pool.get('stock.move') + #Search all confirmed stock_moves and try to assign them + confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed'), ('company_id','=', company.id)], context=context) + move_obj.action_assign(cr, uid, confirmed_ids, context=context) + finally: + if use_new_cursor: + try: + cr.close() + except Exception: + pass + return {} + + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/stock/report/report_stock_move.py b/addons/stock/report/report_stock_move.py index c00031e47e5..4302d080f13 100644 --- a/addons/stock/report/report_stock_move.py +++ b/addons/stock/report/report_stock_move.py @@ -28,7 +28,6 @@ class report_stock_inventory(osv.osv): _name = "report.stock.inventory" _description = "Stock Statistics" _auto = False - _order = 'date desc' def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): res = super(report_stock_inventory, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) product_obj = self.pool.get("product.product") diff --git a/addons/stock/stock.py b/addons/stock/stock.py index b2ccbe13da2..8ac56c43214 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -225,13 +225,15 @@ class stock_quant(osv.osv): # result = self._quants_get(cr, uid, location, product, qty, domain+domain2, context=context) # qty = remaining quants qty - sum(result) domain = domain or [('qty','>',0.0)] - 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, context=context) - elif removal_strategy=='lifo': - result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context) - else: - raise osv.except_osv(_('Error!'),_('Removal strategy %s not implemented.' % (removal_strategy,))) + 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, context=context) + elif removal_strategy=='lifo': + result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context) + else: + raise osv.except_osv(_('Error!'),_('Removal strategy %s not implemented.' % (removal_strategy,))) + return result @@ -296,8 +298,11 @@ class stock_quant(osv.osv): 'cost': 0.0, }, context=context) - quants2 = self.quants_get(cr, uid, False, quant.product_id, quant_neg.qty, [('propagated_from_id','=',quant_neg.id)], 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], { @@ -335,6 +340,8 @@ class stock_quant(osv.osv): offset += 10 return res + + def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None): return self._quants_get_order(cr, uid, location, product, quantity, domain, 'in_date', context=context) @@ -715,13 +722,20 @@ class stock_picking(osv.osv): """ Confirms picking. @return: True """ + proc_group = self.pool.get("procurement.group") self.write(cr, uid, ids, {'state': 'confirmed'}) pickings = self.browse(cr, uid, ids, context=context) todo = [] for picking in pickings: + #If no procurement group, create one + new_proc = False + if not picking.group_id: + new_proc = proc_group.create(cr, uid, {'name': picking.name}, context=context) for r in picking.move_lines: if r.state == 'draft': todo.append(r.id) + if not r.group_id and new_proc: + self.pool.get("stock.move").write(cr, uid, [r.id], {'group_id': new_proc}, context=context) if len(todo): self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context) return True @@ -1799,6 +1813,7 @@ class stock_move(osv.osv): 'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id, 'location_id': move.location_id.id, 'move_id': move.id, + 'group_id': move.group_id and move.group_id.id or False, }) # Check that we do not modify a stock.move which is done @@ -2007,6 +2022,7 @@ class stock_move(osv.osv): if not move.picking_id: # TODO: Put the move in the right picking according to group_id -> should be more elaborated (draft is nok) and picking should be confirmed pick_obj = self.pool.get("stock.picking") + print "Group ID", move.group_id.id picks = pick_obj.search(cr, uid, [('group_id', '=', move.group_id.id), ('location_id', '=', move.location_id.id), ('location_dest_id', '=', move.location_dest_id.id), ('state', 'in', ['confirmed', 'waiting', 'draft'])], context=context) if picks: diff --git a/addons/stock/stock_fifo_lifo.py b/addons/stock/stock_fifo_lifo.py deleted file mode 100644 index a635f391db0..00000000000 --- a/addons/stock/stock_fifo_lifo.py +++ /dev/null @@ -1,218 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp import tools -from openerp.osv import osv, fields - - -class product_product (osv.osv): - _name = "product.product" - _inherit = "product.product" - - def _compute_and_set_avg_price(self, cr, uid, prod, company_id, context=None): - """ - This method is called when we modify the cost method of a product 'prod' from something to 'average'. At that time, we - will compute and set the current average price on the product. - - :param prod: browse_record(product.product) - :param company_id: ID of the company to consider - """ - move_obj = self.pool.get("stock.move") - uom_obj = self.pool.get("product.uom") - qty = 0.0 - total_price = 0.0 - #search all outgoing stock moves and count the total cost and total quantity - mov_ids = move_obj.search(cr, uid, [('product_id', '=', prod.id), ('company_id', '=', company_id), ('state', '=', 'done'), ('location_dest_id.usage', '=', 'internal'), ('location_id.usage', '!=', 'internal')], context=context) - for move in move_obj.browse(cr, uid, mov_ids, context=context): - total_price += move.product_qty * move.price_unit - qty += uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, prod.uom_id.id, round=False) - if qty > 0.0: - self.write(cr, uid, [prod.id], {'standard_price': total_price / qty}, context=context) - - def _update_moves_set_avg_price(self, cr, uid, prod, company_id, context=None): - """ - This method is called when we modify the cost method of a product 'prod' from average to something else. At that time, we - will set the current average price on all the stock moves with a positive remaining quantity. - - :param prod: browse_record(product.product) - :param company_id: ID of the company to consider - """ - move_obj = self.pool.get("stock.move") - uom_obj = self.pool.get("product.uom") - mov_ids = move_obj.search(cr, uid, [('product_id', '=', prod.id), ('company_id', '=', company_id), ('state', '=', 'done'), ('qty_remaining', '>', 0.0)], context=context) - for move in move_obj.browse(cr, uid, mov_ids, context=context): - #convert the average price of the product into the stock move uom - new_price = uom_obj._compute_price(cr, uid, move.product_uom.id, prod.standard_price, prod.uom_id.id) - move_obj.write(cr, uid, [move.id], {'price_unit': new_price}, context=context) - - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - #If we changed the cost method of the product, we may need to do some operation on stock moves or on the product itself - if "cost_method" in vals: - company_id = context.get('force_company', self.pool.get("res.users").browse(cr, uid, uid, context=context).company_id.id) - for prod in self.browse(cr, uid, ids, context=context): - if prod.cost_method == 'average' and vals['cost_method'] != 'average': - #If we are changing the cost_method from 'average' to something else, we need to store the current average price - #on all the done stock move that have a remaining quantity to be matched (mostly incomming stock moves but it may - #include OUT stock moves as well if the stock went into negative) because their further valuation needs to be done - #using this price. - self._update_moves_set_avg_price(cr, uid, prod, company_id, context=context) - elif vals["cost_method"] == 'average' and prod.cost_method != 'average': - #If we are changing the cost_method from anything to 'average', we need to compute the current average price - #and set it on the product as standard_price. - self._compute_and_set_avg_price(cr, uid, prod, company_id, context=context) - return super(product_product, self).write(cr, uid, ids, vals, context=context) - - def get_stock_matchings_fifolifo(self, cr, uid, ids, qty, fifo, product_uom_id=False, currency_id=False, context=None): - # TODO: currency conversions could be omitted - ''' - This method returns a list of tuples with quantities from stock in moves - These are the quantities from the in moves that would go out theoretically according to the fifo or lifo method - (move_in_id, qty in uom of move out, price (converted to move out), qty in uom of move in) - This should be called for only one product at a time - UoMs and currencies from the corresponding moves are converted towards that given in the params - force_company should be used in the context - ''' - assert len(ids) == 1, 'Only the fifolifo stock matchings of one product can be calculated at a time.' - if context is None: - context = {} - uom_obj = self.pool.get('product.uom') - move_obj = self.pool.get('stock.move') - currency_obj = self.pool.get('res.currency') - - product = self.browse(cr, uid, ids, context=context)[0] - if not product_uom_id: - product_uom_id = product.uom_id.id - company_id = context.get('force_company', product.company_id.id) - - if not currency_id: - currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id - if fifo: - order = 'date, id' - else: - order = 'date desc, id desc' #id also for yml tests - move_in_ids = move_obj.search(cr, uid, [('company_id', '=', company_id), - ('qty_remaining', '>', 0.0), - ('state', '=', 'done'), - ('location_id.usage', '!=', 'internal'), - ('location_dest_id.usage', '=', 'internal'), - ('product_id', '=', product.id)], - order = order, context=context) - tuples = [] - qty_to_go = qty - for move in move_obj.browse(cr, uid, move_in_ids, context=context): - #Convert to UoM of product each time - uom_from = move.product_uom.id - qty_from = move.qty_remaining - product_qty = uom_obj._compute_qty(cr, uid, uom_from, qty_from, product_uom_id, round=False) - #Convert currency from in move currency id to out move currency - if move.price_currency_id and (move.price_currency_id.id != currency_id): - new_price = currency_obj.compute(cr, uid, move.price_currency_id.id, currency_id, - move.price_unit, round=False) - else: - new_price = move.price_unit - new_price = uom_obj._compute_price(cr, uid, uom_from, new_price, product_uom_id) - if qty_to_go - product_qty >= 0: - tuples.append((move.id, product_qty, new_price, qty_from),) - qty_to_go -= product_qty - else: - tuples.append((move.id, qty_to_go, new_price, qty_from * qty_to_go / product_qty),) - break - return tuples - - -class stock_move(osv.osv): - _inherit = 'stock.move' - - def _get_moves_from_matchings(self, cr, uid, ids, context=None): - res = set() - for match in self.browse(cr, uid, ids, context=context): - res.add(match.move_out_id.id) - res.add(match.move_in_id.id) - return list(res) - - def _get_qty_remaining (self, cr, uid, ids, field_names, arg, context=None): - ''' - This function calculates how much of the stock move that still needs to be matched - ''' - match_obj = self.pool.get("stock.move.matching") - uom_obj = self.pool.get("product.uom") - res = {} - for move in self.browse(cr, uid, ids, context=context): - qty = move.product_qty - if move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal': - # for incomming moves, the remaining quantity is the quantity what hasn't been matched yet with outgoing moves - matches = match_obj.search(cr, uid, [('move_in_id', '=', move.id)], context=context) - for match in match_obj.browse(cr, uid, matches, context=context): - qty -= uom_obj._compute_qty(cr, uid, match.move_out_id.product_uom.id, match.qty, move.product_uom.id, round=False) - elif move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal': - # for outgoing moves, we need to compute the remaining quantity to manage the negative stocks. - # We do that in the same way that for incomming moves. - matches = match_obj.search(cr, uid, [('move_out_id', '=', move.id)], context=context) - for match in match_obj.browse(cr, uid, matches, context=context): - # we don't need to call uom_obj.compute() as the qty on the matching is already in the uom of the out move - qty -= match.qty - else: - # we don't use remaining quantity on internal moves (no matching are created) - qty = 0 - res[move.id] = qty - return res - - _columns = {'qty_remaining': fields.function(_get_qty_remaining, type="float", string="Remaining quantity to be matched", - store = {'stock.move.matching': (_get_moves_from_matchings, ['qty', 'move_in_id', 'move_out_id'], 10), - 'stock.move': (lambda self, cr, uid, ids, ctx: ids, ['product_qty', 'product_uom', 'location_id', 'location_dest_id'], 10)}), - 'matching_ids_in': fields.one2many('stock.move.matching', 'move_in_id'), - 'matching_ids_out':fields.one2many('stock.move.matching', 'move_out_id'), - } - - -class stock_move_matching(osv.osv): - _name = "stock.move.matching" - _description = "Stock move matchings" - - def _get_unit_price_out (self, cr, uid, ids, field_names, arg, context=None): - res = {} - uom_obj = self.pool.get("product.uom") - for match in self.browse(cr, uid, ids, context=context): - res[match.id] = uom_obj._compute_price(cr, uid, match.move_in_id.product_uom.id, match.price_unit, match.move_out_id.product_uom.id) - return res - - def _get_matches(self, cr, uid, ids, context=None): - move_in_ids = [] - #TOCHECK : self == move_obj i think - move_obj = self.pool.get("stock.move") - for move in move_obj.browse(cr, uid, ids, context=context): - if move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal': - move_in_ids.append(move.id) - return self.pool.get("stock.move.matching").search(cr, uid, [('move_in_id', 'in', move_in_ids)], context=context) - - _columns = { - 'move_in_id': fields.many2one('stock.move', 'Stock move in', required=True), - 'move_out_id': fields.many2one('stock.move', 'Stock move out', required=True), - 'qty': fields.float('Quantity in UoM of out', required=True), - 'price_unit':fields.related('move_in_id', 'price_unit', string="Unit price", type="float"), - 'price_unit_out': fields.function(_get_unit_price_out, type="float", string="Price in UoM of out move", - store = {'stock.move': (_get_matches, ['price_unit', 'product_uom'], 10), - 'stock.move.matching': (lambda self, cr, uid, ids, ctx: ids, ['move_in_id', 'qty', 'move_out_id'], 10)},), - } - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index c1de624c9e8..bd479a1cb6b 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -1023,6 +1023,8 @@ + +