diff --git a/addons/account/wizard/account_chart.py b/addons/account/wizard/account_chart.py index 38df2f7484d..1fbfc6debd6 100644 --- a/addons/account/wizard/account_chart.py +++ b/addons/account/wizard/account_chart.py @@ -51,7 +51,7 @@ class account_chart(osv.osv_memory): FROM account_period p LEFT JOIN account_fiscalyear f ON (p.fiscalyear_id = f.id) WHERE f.id = %s - ORDER BY p.date_start ASC + ORDER BY p.date_start ASC, p.special DESC LIMIT 1) AS period_start UNION ALL SELECT * FROM (SELECT p.id diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py index 8b93e2a9bb6..8c18023325f 100644 --- a/addons/mrp/procurement.py +++ b/addons/mrp/procurement.py @@ -24,6 +24,8 @@ from dateutil.relativedelta import relativedelta from openerp.osv import fields from openerp.osv import osv from openerp.tools.translate import _ +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT + class procurement_order(osv.osv): _inherit = 'procurement.order' @@ -77,35 +79,43 @@ class procurement_order(osv.osv): res = procurement_obj.make_mo(cr, uid, ids, context=context) res = res.values() return len(res) and res[0] or 0 - + + def _get_date_planned(self, cr, uid, procurement, context=None): + format_date_planned = datetime.strptime(procurement.date_planned, + DEFAULT_SERVER_DATETIME_FORMAT) + date_planned = format_date_planned - relativedelta(days=procurement.product_id.produce_delay or 0.0) + date_planned = date_planned - relativedelta(days=procurement.company_id.manufacturing_lead) + return date_planned + + def _prepare_mo_vals(self, cr, uid, procurement, context=None): + res_id = procurement.move_id.id + newdate = self._get_date_planned(cr, uid, procurement, context=context) + return { + 'origin': procurement.origin, + 'product_id': procurement.product_id.id, + 'product_qty': procurement.product_qty, + 'product_uom': procurement.product_uom.id, + 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False, + 'product_uos': procurement.product_uos and procurement.product_uos.id or False, + 'location_src_id': procurement.location_id.id, + 'location_dest_id': procurement.location_id.id, + 'bom_id': procurement.bom_id and procurement.bom_id.id or False, + 'date_planned': newdate.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'move_prod_id': res_id, + 'company_id': procurement.company_id.id, + } + def make_mo(self, cr, uid, ids, context=None): """ Make Manufacturing(production) order from procurement @return: New created Production Orders procurement wise """ res = {} - company = self.pool.get('res.users').browse(cr, uid, uid, context).company_id production_obj = self.pool.get('mrp.production') move_obj = self.pool.get('stock.move') procurement_obj = self.pool.get('procurement.order') for procurement in procurement_obj.browse(cr, uid, ids, context=context): - res_id = procurement.move_id.id - newdate = datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.product_id.produce_delay or 0.0) - newdate = newdate - relativedelta(days=company.manufacturing_lead) - produce_id = production_obj.create(cr, uid, { - 'origin': procurement.origin, - 'product_id': procurement.product_id.id, - 'product_qty': procurement.product_qty, - 'product_uom': procurement.product_uom.id, - 'product_uos_qty': procurement.product_uos and procurement.product_uos_qty or False, - 'product_uos': procurement.product_uos and procurement.product_uos.id or False, - 'location_src_id': procurement.location_id.id, - 'location_dest_id': procurement.location_id.id, - 'bom_id': procurement.bom_id and procurement.bom_id.id or False, - 'date_planned': newdate.strftime('%Y-%m-%d %H:%M:%S'), - 'move_prod_id': res_id, - 'company_id': procurement.company_id.id, - }) - + vals = self._prepare_mo_vals(cr, uid, procurement, context=context) + produce_id = production_obj.create(cr, uid, vals, context=context) res[procurement.id] = produce_id self.write(cr, uid, [procurement.id], {'state': 'running', 'production_id': produce_id}) bom_result = production_obj.action_compute(cr, uid, diff --git a/addons/product/product.py b/addons/product/product.py index 3937d8ed092..dec91506883 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -637,11 +637,8 @@ class product_product(osv.osv): return result def _get_name_template_ids(self, cr, uid, ids, context=None): - result = set() template_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)]) - for el in template_ids: - result.add(el) - return list(result) + return list(set(template_ids)) _columns = { 'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'), diff --git a/addons/purchase/tests/__init__.py b/addons/purchase/tests/__init__.py new file mode 100644 index 00000000000..295471b9aa3 --- /dev/null +++ b/addons/purchase/tests/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# 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 . +# +############################################################################## + +import test_average_price + +checks = [ + test_average_price, +] diff --git a/addons/purchase/tests/test_average_price.py b/addons/purchase/tests/test_average_price.py new file mode 100644 index 00000000000..4cc4849a33e --- /dev/null +++ b/addons/purchase/tests/test_average_price.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# 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.test.common import TransactionCase +from datetime import date +from openerp.tests import common +from openerp import netsvc + + +class TestAveragePrice(common.TransactionCase): + def setUp(self): + super(TestAveragePrice, self).setUp() + cr, uid, context = self.cr, self.uid, {} + self.ir_model_data = self.registry('ir.model.data') + self.product_product = self.registry('product.product') + self.purchase_order = self.registry('purchase.order') + self.purchase_order_line = self.registry('purchase.order.line') + self.pricelist = self.registry('product.pricelist') + self.stock_location = self.registry('stock.location') + self.stock_picking = self.registry('stock.picking') + self.stock_move = self.registry('stock.move') + self.stock_partial_move = self.registry('stock.partial.move') + self.stock_partial_move_line = self.registry('stock.partial.move.line') + self.partial_picking = self.registry('stock.partial.picking') + self.partial_picking_line = self.registry('stock.partial.picking.line') + change_product_qty = self.registry('stock.change.product.qty') + + _, partner_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'res_partner_1') + _, pricelist_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'product', 'list0') + _, self.location_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock') + _, self.supplier_location_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_suppliers') + _, input_account_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'xfa') + _, output_account_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'xfa') + wf_service = netsvc.LocalService("workflow") + + self.standard_price = 10 + self.order_price_unit = 20 + self.available_qty = 1 + self.order_qty = 1 + self.picking_qty = 1 + + self.product_id = self.product_product.create(cr, uid, { + 'name': 'Average product', + 'cost_method': 'average', + 'valuation': 'real_time', + 'property_stock_account_input': input_account_id, + 'property_stock_account_output': output_account_id, + }, context=context) + + self.product_product.do_change_standard_price( + cr, uid, [self.product_id], { + 'new_price': self.standard_price, + 'stock_input_account': input_account_id, + 'stock_output_account': output_account_id}) + + change_product_qty_id = change_product_qty.create( + cr, uid, { + 'location_id': self.location_id, + 'new_quantity': self.available_qty, + 'product_id': self.product_id}) + change_product_qty.change_product_qty( + cr, uid, [change_product_qty_id], { + 'active_model': 'product.product', + 'active_id': self.product_id, + 'active_ids': [self.product_id]}) + + self.po_01_id = self.purchase_order.create(cr, uid, { + 'partner_id': partner_id, + 'location_id': self.location_id, + 'pricelist_id': pricelist_id, + }, context=context) + + self.order_line_10 = self.purchase_order_line.create(cr, uid, { + 'order_id': self.po_01_id, + 'product_id': self.product_id, + 'name': 'description', + 'date_planned': date.today(), + 'product_qty': self.order_qty, + 'price_unit': self.order_price_unit + }, context=context) + + wf_service.trg_validate(uid, 'purchase.order', self.po_01_id, 'purchase_confirm', cr) + + + def test_10_stock_move_action_done(self): + cr, uid, context = self.cr, self.uid, {} + picking_id = self.purchase_order.read(cr, uid, [self.po_01_id], ['picking_ids'])[0]['picking_ids'] + move_lines_ids = self.stock_picking.read(cr, uid, picking_id, ['move_lines'])[0]['move_lines'] + for move in self.stock_move.browse(cr, uid, move_lines_ids, context=context): + move.action_done() + + new_price = self.product_product.read(cr, uid, self.product_id, ['standard_price'], context=context)['standard_price'] + self.assertAlmostEqual( + new_price, + (self.available_qty * self.standard_price + self.order_qty * self.order_price_unit) + /(self.available_qty + self.order_qty)) + + def test_20_partial_stock_move(self): + cr, uid, context = self.cr, self.uid, {} + picking_ids = self.purchase_order.read(cr, uid, [self.po_01_id], ['picking_ids'])[0]['picking_ids'] + product = self.product_product.browse(cr, uid, self.product_id, context=context) + + partial_move_id = self.stock_partial_move.create(cr, uid, { + 'date': date.today(), + 'picking_id': picking_ids[0] + }, context=context) + + move_lines_ids = self.stock_picking.read(cr, uid, picking_ids, ['move_lines'])[0]['move_lines'] + for move in self.stock_move.browse(cr, uid, move_lines_ids, context=context): + self.stock_partial_move_line.create(cr, uid, { + 'product_id': self.product_id, + 'quantity': self.picking_qty, + 'product_uom': product.uom_id.id, + 'location_dest_id': self.location_id, + 'location_id': self.supplier_location_id, + 'move_id': move.id, + 'cost': self.order_price_unit, + 'wizard_id': partial_move_id, + }, context=context) + + self.stock_partial_move.do_partial(cr, uid, [partial_move_id], context=context) + + new_price = self.product_product.read(cr, uid, self.product_id, ['standard_price'], context=context)['standard_price'] + self.assertAlmostEqual( + new_price, + (self.available_qty * self.standard_price + self.order_qty * self.order_price_unit) + /(self.available_qty + self.order_qty)) + + def test_30_partial_stock_picking(self): + cr, uid, context = self.cr, self.uid, {} + picking_ids = self.purchase_order.read(cr, uid, [self.po_01_id], ['picking_ids'])[0]['picking_ids'] + product = self.product_product.browse(cr, uid, self.product_id, context=context) + + partial_picking_id = self.partial_picking.create(cr, uid, { + 'date': date.today(), + 'picking_id': picking_ids[0], + }, context=context) + + move_lines_ids = self.stock_picking.read(cr, uid, picking_ids, ['move_lines'])[0]['move_lines'] + for move in self.stock_move.browse(cr, uid, move_lines_ids, context=context): + self.partial_picking_line.create(cr, uid, { + 'product_id': self.product_id, + 'quantity': self.picking_qty, + 'product_uom': product.uom_id.id, + 'location_dest_id': self.location_id, + 'location_id': self.supplier_location_id, + 'move_id': move.id, + 'cost': self.order_price_unit, + 'wizard_id': partial_picking_id, + }, context=context) + + self.partial_picking.do_partial(cr, uid, [partial_picking_id], context=context) + + new_price = self.product_product.read(cr, uid, self.product_id, ['standard_price'], context=context)['standard_price'] + self.assertAlmostEqual( + new_price, + (self.available_qty * self.standard_price + self.order_qty * self.order_price_unit) + /(self.available_qty + self.order_qty)) + diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 99ee036f2e7..379d252854b 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1227,8 +1227,6 @@ class stock_picking(osv.osv): context = dict(context) res = {} move_obj = self.pool.get('stock.move') - product_obj = self.pool.get('product.product') - currency_obj = self.pool.get('res.currency') uom_obj = self.pool.get('product.uom') sequence_obj = self.pool.get('ir.sequence') for pick in self.browse(cr, uid, ids, context=context): @@ -1256,40 +1254,12 @@ class stock_picking(osv.osv): else: too_many.append(move) - # Average price computation if (pick.type == 'in') and (move.product_id.cost_method == 'average'): - product = product_obj.browse(cr, uid, move.product_id.id) - move_currency_id = move.company_id.currency_id.id - context['currency_id'] = move_currency_id - qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) - - if product.id not in product_avail: - # keep track of stock on hand including processed lines not yet marked as done - product_avail[product.id] = product.qty_available - - if qty > 0: - new_price = currency_obj.compute(cr, uid, product_currency, - move_currency_id, product_price, round=False) - new_price = uom_obj._compute_price(cr, uid, product_uom, new_price, - product.uom_id.id) - if product_avail[product.id] <= 0: - product_avail[product.id] = 0 - new_std_price = new_price - else: - # Get the standard price - amount_unit = product.price_get('standard_price', context=context)[product.id] - new_std_price = ((amount_unit * product_avail[product.id])\ - + (new_price * qty))/(product_avail[product.id] + qty) - # Write the field according to price type field - product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price}) - - # Record the values that were chosen in the wizard, so they can be - # used for inventory valuation if real-time valuation is enabled. - move_obj.write(cr, uid, [move.id], - {'price_unit': product_price, - 'price_currency_id': product_currency}) - - product_avail[product.id] += qty + # Record the values that were chosen in the wizard, so they can be + # used for average price computation and inventory valuation + move_obj.write(cr, uid, [move.id], + {'price_unit': product_price, + 'price_currency_id': product_currency}) # every line of the picking is empty, do not generate anything empty_picking = not any(q for q in move_product_qty.values() if q > 0) @@ -2328,6 +2298,44 @@ class stock_move(osv.osv): return reference_amount, reference_currency_id + def _update_average_price(self, cr, uid, move, context=None): + product_obj = self.pool.get('product.product') + currency_obj = self.pool.get('res.currency') + uom_obj = self.pool.get('product.uom') + product_avail = {} + + if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'): + product = product_obj.browse(cr, uid, move.product_id.id) + move_currency_id = move.company_id.currency_id.id + context['currency_id'] = move_currency_id + + product_qty = move.product_qty + product_uom = move.product_uom.id + product_price = move.price_unit + product_currency = move.price_currency_id.id + + if product.id not in product_avail: + # keep track of stock on hand including processed lines not yet marked as done + product_avail[product.id] = product.qty_available + + qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) + if qty > 0: + new_price = currency_obj.compute(cr, uid, product_currency, + move_currency_id, product_price, round=False) + new_price = uom_obj._compute_price(cr, uid, product_uom, new_price, + product.uom_id.id) + if product_avail[product.id] <= 0: + product_avail[product.id] = 0 + new_std_price = new_price + else: + # Get the standard price + amount_unit = product.price_get('standard_price', context=context)[product.id] + new_std_price = ((amount_unit * product_avail[product.id])\ + + (new_price * qty))/(product_avail[product.id] + qty) + + product_obj.write(cr, uid, [product.id],{'standard_price': new_std_price}) + + product_avail[product.id] += qty def _create_product_valuation_moves(self, cr, uid, move, context=None): """ @@ -2414,6 +2422,7 @@ class stock_move(osv.osv): if move.move_dest_id.auto_validate: self.action_done(cr, uid, [move.move_dest_id.id], context=context) + self._update_average_price(cr, uid, move, context=context) self._create_product_valuation_moves(cr, uid, move, context=context) if move.state not in ('confirmed','done','assigned'): todo.append(move.id) @@ -2676,9 +2685,6 @@ class stock_move(osv.osv): """ res = {} picking_obj = self.pool.get('stock.picking') - product_obj = self.pool.get('product.product') - currency_obj = self.pool.get('res.currency') - uom_obj = self.pool.get('product.uom') if context is None: context = {} @@ -2704,33 +2710,13 @@ class stock_move(osv.osv): else: too_many.append(move) - # Average price computation if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'): - product = product_obj.browse(cr, uid, move.product_id.id) - move_currency_id = move.company_id.currency_id.id - context['currency_id'] = move_currency_id - qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) - if qty > 0: - new_price = currency_obj.compute(cr, uid, product_currency, - move_currency_id, product_price, round=False) - new_price = uom_obj._compute_price(cr, uid, product_uom, new_price, - product.uom_id.id) - if product.qty_available <= 0: - new_std_price = new_price - else: - # Get the standard price - amount_unit = product.price_get('standard_price', context=context)[product.id] - new_std_price = ((amount_unit * product.qty_available)\ - + (new_price * qty))/(product.qty_available + qty) - - product_obj.write(cr, uid, [product.id],{'standard_price': new_std_price}) - - # Record the values that were chosen in the wizard, so they can be - # used for inventory valuation if real-time valuation is enabled. - self.write(cr, uid, [move.id], - {'price_unit': product_price, - 'price_currency_id': product_currency, - }) + # Record the values that were chosen in the wizard, so they can be + # used for average price computation and inventory valuation + self.write(cr, uid, [move.id], + {'price_unit': product_price, + 'price_currency_id': product_currency, + }) for move in too_few: product_qty = move_product_qty[move.id] diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 4694b405d14..fc9a56e4bda 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -1608,7 +1608,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we .filter(function (column) { return column.tag === 'field';}) .pluck('name').value(), function (groups) { - self.view.$pager.hide(); + // page count is irrelevant on grouped page, replace by limit + self.view.$pager.find('.oe_pager_group').hide(); + self.view.$pager.find('.oe_list_pager_state').text(self.view._limit ? self.view._limit : '∞'); $el[0].appendChild( self.render_groups(groups)); if (post_render) { post_render(); }