diff --git a/stock_inventory_revaluation/README.rst b/stock_inventory_revaluation/README.rst new file mode 100644 index 000000000..cceeedcd5 --- /dev/null +++ b/stock_inventory_revaluation/README.rst @@ -0,0 +1,85 @@ +.. image:: https://img.shields.io/badge/license-AGPLv3-blue.svg + :target: https://www.gnu.org/licenses/agpl.html + :alt: License: AGPL-3 + +=================================== +Stock Account Inventory Revaluation +=================================== + +If your company runs a perpetual inventory system, you may need to perform +inventory revaluation. + +You can re-valuate inventory values by: + +* Changing the price for a specific product. The inventory price is changed + and inventory value is recalculated according to the new price. In case of + real price, you can select which quants you want to change the price on. + +* Changing the value of the inventory. The quantity of inventory remains + unchanged, resulting in a change in the price. + + + +Configuration +============= + + +* Go to *Inventory / Configuration / Products / Product Categories* and + define, for each category, a Valuation Increase Account and a Valuation + Decrease Account. These accounts will be used as contra-accounts to the + Stock Valuation Account during the inventory re-valuation. + +* Users willing to access to the Inventory Revaluation menu should be + members of the group "Manage Inventory Valuation and Costing Methods". + + +Usage +===== + +* Go to *Inventory / Inventory Control / Inventory Revaluation / Products* + to create a new Inventory Revaluation. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/154/8.0 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In +case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and +welcomed `feedback `_. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Eficent Business and IT Consulting Services S.L. + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. \ No newline at end of file diff --git a/stock_inventory_revaluation/__init__.py b/stock_inventory_revaluation/__init__.py new file mode 100644 index 000000000..8c649e73e --- /dev/null +++ b/stock_inventory_revaluation/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from . import models +from . import wizards diff --git a/stock_inventory_revaluation/__openerp__.py b/stock_inventory_revaluation/__openerp__.py new file mode 100644 index 000000000..1f6389b8c --- /dev/null +++ b/stock_inventory_revaluation/__openerp__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Stock Inventory Revaluation", + "summary": "Introduces inventory revaluation as single point to change " + "the valuation of products.", + "version": "8.0.1.0.0", + "author": "Eficent Business and IT Consulting Services S.L., " + "Odoo Community Association (OCA)", + "website": "http://www.eficent.com", + "category": "Warehouse", + "depends": ["stock_account"], + "license": "AGPL-3", + "data": [ + "wizards/stock_inventory_revaluation_get_quants_view.xml", + "security/stock_inventory_revaluation_security.xml", + "security/ir.model.access.csv", + "views/stock_inventory_revaluation_view.xml", + "views/product_view.xml", + "data/stock_inventory_revaluation_data.xml", + ], + 'installable': True, +} diff --git a/stock_inventory_revaluation/data/stock_inventory_revaluation_data.xml b/stock_inventory_revaluation/data/stock_inventory_revaluation_data.xml new file mode 100644 index 000000000..4ab5d3c02 --- /dev/null +++ b/stock_inventory_revaluation/data/stock_inventory_revaluation_data.xml @@ -0,0 +1,21 @@ + + + + + + Stock Inventory Revaluation + stock.inventory.revaluation + + + + Stock Inventory Revaluation + stock.inventory.revaluation + IR/ + 5 + 1 + 1 + + + + diff --git a/stock_inventory_revaluation/models/__init__.py b/stock_inventory_revaluation/models/__init__.py new file mode 100644 index 000000000..ac3d5ed10 --- /dev/null +++ b/stock_inventory_revaluation/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import product +from . import stock_inventory_revaluation diff --git a/stock_inventory_revaluation/models/product.py b/stock_inventory_revaluation/models/product.py new file mode 100644 index 000000000..c523ca54f --- /dev/null +++ b/stock_inventory_revaluation/models/product.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from openerp import fields, models + + +class ProductCategory(models.Model): + + _inherit = 'product.category' + + property_inventory_revaluation_increase_account_categ = fields.Many2one( + 'account.account', string='Valuation Increase Account', + company_dependent=True, + help="Define the G/L accounts to be used as the balancing account in " + "the transaction created by the revaluation. The G/L Increase " + "Account is used when the inventory value is increased due to " + "the revaluation.") + + property_inventory_revaluation_decrease_account_categ = fields.Many2one( + 'account.account', string='Valuation Decrease Account', + company_dependent=True, + help="Define the G/L accounts to be used as the balancing account in " + "the transaction created by the revaluation. The G/L Decrease " + "Account is used when the inventory value is decreased.") diff --git a/stock_inventory_revaluation/models/stock_inventory_revaluation.py b/stock_inventory_revaluation/models/stock_inventory_revaluation.py new file mode 100644 index 000000000..85675f1a6 --- /dev/null +++ b/stock_inventory_revaluation/models/stock_inventory_revaluation.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from openerp import api, fields, models, _ +import openerp.addons.decimal_precision as dp +import time +from openerp.exceptions import Warning as UserError + +_STATES = [ + ('draft', 'Draft'), + ('posted', 'Posted'), + ('cancel', 'Cancelled')] + + +class StockInventoryRevaluation(models.Model): + + _name = 'stock.inventory.revaluation' + _description = 'Inventory revaluation' + + @api.model + def _default_journal(self): + res = self.env['account.journal'].search([('type', '=', 'general')]) + return res and res[0] or False + + name = fields.Char('Reference', + help="Reference for the journal entry", + readonly=True, + required=True, + states={'draft': [('readonly', False)]}, + copy=False, + default='/') + + revaluation_type = fields.Selection( + [('price_change', 'Price Change'), + ('inventory_value', 'Inventory Debit/Credit')], + string="Revaluation Type", + readonly=True, required=True, + default='price_change', + help="'Price Change': You can re-valuate inventory values by Changing " + "the price for a specific product. The inventory price is " + "changed and inventory value is recalculated according to the " + "new price.\n " + "'Inventory Debit/Credit': Changing the value of the inventory. " + "The quantity of inventory remains unchanged, resulting in a " + "change in the price", + states={'draft': [('readonly', False)]}) + + remarks = fields.Text('Remarks', + help="Displays by default Inventory Revaluation. " + "This text is copied to the journal entry.", + readonly=True, + default='Inventory Revaluation', + states={'draft': [('readonly', False)]}) + + state = fields.Selection(selection=_STATES, + string='Status', + readonly=True, + required=True, + default='draft', + states={'draft': [('readonly', False)]}) + + company_id = fields.Many2one( + comodel_name='res.company', string='Company', readonly=True, + default=lambda self: self.env['res.company']._company_default_get( + 'stock.inventory.revaluation'), + states={'draft': [('readonly', False)]}) + + document_date = fields.Date( + 'Creation date', required=True, readonly=True, + default=lambda self: fields.Date.context_today(self), + states={'draft': [('readonly', False)]}) + + journal_id = fields.Many2one('account.journal', 'Journal', + default=_default_journal, + readonly=True, + states={'draft': [('readonly', False)]}) + + line_ids = fields.One2many('stock.inventory.revaluation.line', + 'revaluation_id', + string='Revaluation lines', + readonly=False, + states={'posted': [('readonly', True)]}) + + @api.model + def create(self, values): + sequence_obj = self.env['ir.sequence'] + if values.get('name', '/') == '/': + values['name'] = sequence_obj.get('stock.inventory.revaluation') + return super(StockInventoryRevaluation, self).create(values) + + @api.one + def post(self): + for line in self.line_ids: + if line.product_template_id.valuation != 'real_time': + continue + line.post() + return True + + @api.multi + def button_post(self): + self.post() + self.write({'state': 'posted'}) + return True + + @api.multi + def button_draft(self): + self.write({'state': 'draft'}) + return True + + @api.multi + def button_cancel(self): + moves = self.env['account.move'] + for line in self.line_ids: + if line.move_id: + moves += line.move_id + for line_quant in line.line_quant_ids: + if line_quant.move_id: + moves += line_quant.move_id + line_quant.quant_id.write({'cost': line_quant.old_cost}) + if moves: + # second, invalidate the move(s) + moves.button_cancel() + # delete the move this revaluation was pointing to + # Note that the corresponding move_lines and move_reconciles + # will be automatically deleted too + moves.unlink() + self.write({'state': 'cancel'}) + return True + + +class StockInventoryRevaluationLine(models.Model): + + _name = 'stock.inventory.revaluation.line' + _description = 'Inventory revaluation line' + + @api.one + def _get_product_template_qty(self): + self.qty_available = 0 + for prod_variant in self.product_template_id.product_variant_ids: + self.qty_available += prod_variant.qty_available + + @api.one + def _calc_product_template_value(self): + qty_available = 0 + current_value = 0.0 + quant_obj = self.env['stock.quant'] + for prod_variant in self.product_template_id.product_variant_ids: + qty_available += prod_variant.qty_available + if self.product_template_id.cost_method == 'real': + quants = quant_obj.search([('product_id', '=', + prod_variant.id), + ('location_id.usage', '=', + 'internal')]) + for quant in quants: + current_value += quant.cost + else: + current_value = \ + self.product_template_id.standard_price * qty_available + self.current_value = current_value + + @api.one + @api.depends("product_template_id", "product_template_id.standard_price") + def _calc_current_cost(self): + self.current_cost = self.product_template_id.standard_price + + revaluation_id = fields.Many2one('stock.inventory.revaluation', + 'Stock Inventory Revaluation', + required=True, + ondelete='cascade') + + state = fields.Selection(selection=_STATES, + string='UoM', readonly=True, + related="revaluation_id.state") + + product_template_id = fields.Many2one('product.template', 'Product', + required=True, + domain=[('type', '=', 'product')]) + + cost_method = fields.Selection(string="Cost Method", readonly=True, + related='product_template_id.cost_method') + + uom_id = fields.Many2one('product.uom', 'UoM', readonly=True, + related="product_template_id.uom_id") + + old_cost = fields.Float('Old cost', + help='Displays the previous cost of the ' + 'product.', + digits=dp.get_precision('Product Price'), + readonly=True) + + current_cost = fields.Float('Current cost', + help='Displays the current cost of the ' + 'product.', + digits=dp.get_precision('Product Price'), + compute="_calc_current_cost", + readonly=True) + + new_cost = fields.Float('New cost', + help="Enter the new cost you wish to assign to " + "the product. Relevant only when the " + "selected revaluation type is Price Change.", + digits=dp.get_precision('Product Price')) + + current_value = fields.Float('Current value', + help='Displays the current value of the ' + 'product.', + digits=dp.get_precision('Account'), + compute="_calc_product_template_value", + readonly=True) + + old_value = fields.Float('Old value', + help='Displays the current value of the product.', + digits=dp.get_precision('Account'), + readonly=True) + + new_value = fields.Float('Credit/Debit amount', + help="Enter the amount you wish to credit or " + "debit from the current inventory value of " + "the item. Enter credit as a negative value." + "Relevant only if the selected revaluation " + "type is Inventory Credit/Debit.", + digits=dp.get_precision('Account')) + + qty_available = fields.Float( + 'Quantity On Hand', compute='_get_product_template_qty', + digits_compute=dp.get_precision('Product Unit of Measure')) + + increase_account_id = fields.Many2one( + 'account.account', 'Increase Account', + help="Define the G/L accounts to be used as the balancing account in " + "the transaction created by the revaluation. The Increase " + "Account is used when the inventory value is increased due to " + "the revaluation.") + + decrease_account_id = fields.Many2one( + 'account.account', 'Decrease Account', + help="Define the G/L accounts to be used as the balancing account in " + "the transaction created by the revaluation. The Decrease " + "Account is used when the inventory value is decreased.") + + company_id = fields.Many2one( + comodel_name='res.company', string='Company', readonly=True, + related="revaluation_id.company_id") + + move_id = fields.Many2one('account.move', 'Account move', readonly=True) + + revaluation_type = fields.Selection( + string="Revaluation Type", readonly=True, + related='revaluation_id.revaluation_type', + default='price_change') + + line_quant_ids = fields.One2many('stock.inventory.revaluation.line.quant', + 'line_id', + string='Revaluation line quants') + + _sql_constraints = [ + ('inv_valu_line_prod_temp_uniq', + 'unique (revaluation_id, product_template_id)', + _('Cannot enter the same product multiple times in the same ' + 'inventory valuation!'))] + + @api.one + @api.constrains('product_template_id', 'company_id') + def _check_is_stockable(self): + if self.product_template_id.type != 'product': + raise UserError(_('Configuration error!\nThe product must be ' + 'stockable.')) + + @api.one + @api.onchange("product_template_id") + def _onchange_product_template_id(self): + if self.product_template_id: + self.increase_account_id = self.product_template_id.categ_id and \ + self.product_template_id.categ_id.\ + property_inventory_revaluation_increase_account_categ + self.decrease_account_id = self.product_template_id.categ_id and \ + self.product_template_id.categ_id.\ + property_inventory_revaluation_decrease_account_categ + self.revaluation_type = self.revaluation_id.revaluation_type + + @api.model + def _prepare_move_data(self, date_move): + + period = self.env['account.period'].find(date_move)[0] + + return { + 'narration': self.revaluation_id.remarks, + 'date': date_move, + 'ref': self.revaluation_id.name, + 'journal_id': self.revaluation_id.journal_id.id, + 'period_id': period.id, + } + + @api.model + def _prepare_debit_move_line_data(self, amount, account_id, prod_id): + return { + 'name': self.revaluation_id.name, + 'date': self.move_id.date, + 'product_id': prod_id, + 'account_id': account_id, + 'move_id': self.move_id.id, + 'debit': amount + } + + @api.model + def _prepare_credit_move_line_data(self, amount, account_id, prod_id): + return { + 'name': self.revaluation_id.name, + 'date': self.move_id.date, + 'product_id': prod_id, + 'account_id': account_id, + 'move_id': self.move_id.id, + 'credit': amount + } + + @api.model + def _create_accounting_entry(self, amount_diff): + timenow = time.strftime('%Y-%m-%d') + move_data = self._prepare_move_data(timenow) + datas = self.env['product.template'].get_product_accounts( + self.product_template_id.id) + self.move_id = self.env['account.move'].create(move_data).id + move_line_obj = self.env['account.move.line'] + + if not self.decrease_account_id or not self.increase_account_id: + raise UserError(_("Please add a Increase Account and " + "a Decrease Account.")) + + for prod_variant in self.product_template_id.product_variant_ids: + qty = prod_variant.qty_available + if qty: + if amount_diff > 0: + debit_account_id = self.decrease_account_id.id + credit_account_id = \ + datas['property_stock_valuation_account_id'] + else: + debit_account_id = \ + datas['property_stock_valuation_account_id'] + credit_account_id = self.increase_account_id.id + move_line_data = self._prepare_debit_move_line_data( + abs(amount_diff), debit_account_id, prod_variant.id) + move_line_obj.create(move_line_data) + move_line_data = self._prepare_credit_move_line_data( + abs(amount_diff), credit_account_id, prod_variant.id) + move_line_obj.create(move_line_data) + if self.move_id.journal_id.entry_posted: + self.move_id.post() + + @api.one + def post(self): + + amount_diff = 0.0 + if self.product_template_id.cost_method == 'real': + for line_quant in self.line_quant_ids: + amount_diff += line_quant.get_total_value() + line_quant.write_new_cost() + if amount_diff == 0.0: + return True + else: + if self.product_template_id.cost_method in ['standard', 'average']: + + if self.revaluation_id.revaluation_type == 'price_change': + diff = self.current_cost - self.new_cost + amount_diff = self.qty_available * diff + else: + amount_diff = self.current_value - self.new_value + if self.new_value < 0: + raise UserError(_("The new value for product %s " + "cannot be negative" + % self.product_template_id.name)) + if self.qty_available <= 0.0: + raise UserError( + _("Cannot do an inventory value change if the " + "quantity available for product %s " + "is 0 or negative" % + self.product_template_id.name)) + + if self.revaluation_id.revaluation_type == 'price_change': + self.old_cost = self.current_cost + self.product_template_id.write({'standard_price': + self.new_cost}) + else: + self.old_cost = self.current_cost + self.old_value = self.current_value + value_diff = self.current_value - self.new_value + new_cost = value_diff / self.qty_available + self.product_template_id.write({'standard_price': + new_cost}) + + if self.product_template_id.valuation == 'real_time': + self._create_accounting_entry(amount_diff) + + +class StockInventoryRevaluationLineQuant(models.Model): + + _name = 'stock.inventory.revaluation.line.quant' + _description = 'Inventory revaluation line quant' + + line_id = fields.Many2one('stock.inventory.revaluation.line', + 'Revaluation Line', required=True, + readonly=True) + + quant_id = fields.Many2one('stock.quant', 'Quant', required=True, + readonly=True, + domain=[('product_id.type', '=', 'product')]) + + product_id = fields.Many2one('product.product', 'Product', + readonly=True, + related="quant_id.product_id") + + location_id = fields.Many2one('stock.location', 'Location', + readonly=True, + related="quant_id.location_id") + + qty = fields.Float('Quantity', readonly=True, + related="quant_id.qty") + + in_date = fields.Datetime('Incoming Date', readonly=True, + related="quant_id.in_date") + + current_cost = fields.Float('Current Cost', + readonly=True, + related="quant_id.cost") + + old_cost = fields.Float('Previous cost', + help='Shows the previous cost of the quant', + readonly=True) + + new_cost = fields.Float('New Cost', + help="Enter the new cost you wish to assign to " + "the Quant. Relevant only when the " + "selected revaluation type is Price Change.", + digits=dp.get_precision('Product Price'), + copy=False) + + def get_total_value(self): + amount_diff = 0.0 + if self.product_id.product_tmpl_id.cost_method == 'real': + if self.line_id.revaluation_id.revaluation_type != 'price_change': + raise UserError(_("You can only post quant cost changes.")) + else: + diff = self.current_cost - self.new_cost + amount_diff = self.qty * diff + return amount_diff + + def write_new_cost(self): + self.old_cost = self.current_cost + self.quant_id.write({'cost': self.new_cost}) + return True diff --git a/stock_inventory_revaluation/security/ir.model.access.csv b/stock_inventory_revaluation/security/ir.model.access.csv new file mode 100644 index 000000000..e397f0906 --- /dev/null +++ b/stock_inventory_revaluation/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_inventory_revaluation,stock.inventory.revaluation,model_stock_inventory_revaluation,stock_account.group_inventory_valuation,1,1,1,1 +access_stock_inventory_revaluation_line,stock.inventory.revaluation.line,model_stock_inventory_revaluation_line,stock_account.group_inventory_valuation,1,1,1,1 +access_stock_inventory_revaluation_line_quant,stock.inventory.revaluation.line.quant,model_stock_inventory_revaluation_line_quant,stock_account.group_inventory_valuation,1,1,1,1 \ No newline at end of file diff --git a/stock_inventory_revaluation/security/stock_inventory_revaluation_security.xml b/stock_inventory_revaluation/security/stock_inventory_revaluation_security.xml new file mode 100644 index 000000000..1d59b4afa --- /dev/null +++ b/stock_inventory_revaluation/security/stock_inventory_revaluation_security.xml @@ -0,0 +1,22 @@ + + + + + + Stock Inventory Revaluation multi-company + + + ['|',('company_id','=',False), + ('company_id','child_of',[user.company_id.id])] + + + + Stock Inventory Revaluation line multi-company + + + ['|',('company_id','=',False), + ('company_id','child_of',[user.company_id.id])] + + + + diff --git a/stock_inventory_revaluation/static/description/icon.png b/stock_inventory_revaluation/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/stock_inventory_revaluation/static/description/icon.png differ diff --git a/stock_inventory_revaluation/tests/__init__.py b/stock_inventory_revaluation/tests/__init__.py new file mode 100644 index 000000000..32858f1ba --- /dev/null +++ b/stock_inventory_revaluation/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_stock_inventory_revaluation diff --git a/stock_inventory_revaluation/tests/test_stock_inventory_revaluation.py b/stock_inventory_revaluation/tests/test_stock_inventory_revaluation.py new file mode 100644 index 000000000..cb5626410 --- /dev/null +++ b/stock_inventory_revaluation/tests/test_stock_inventory_revaluation.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# © 2015 Eficent Business and IT Consulting Services S.L. +# - Jordi Ballester Alomar +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp.tests.common import TransactionCase +from datetime import datetime +from datetime import date, timedelta + + +class TestStockInventoryRevaluation(TransactionCase): + """Test that the Inventory is Revaluated when the + inventory price for any product is changed.""" + + def setUp(self): + super(TestStockInventoryRevaluation, self).setUp() + # Get required Model + self.product_model = self.env['product.product'] + self.product_ctg_model = self.env['product.category'] + self.reval_model = self.env['stock.inventory.revaluation'] + self.reval_line_model = self.env['stock.inventory.revaluation.line'] + self.account_model = self.env['account.account'] + self.acc_type_model = self.env['account.account.type'] + self.reval_line_quant_model = self.\ + env['stock.inventory.revaluation.line.quant'] + self.get_quant_model = self.\ + env['stock.inventory.revaluation.line.get.quant'] + self.stock_change_model = self.env['stock.change.product.qty'] + self.stock_lot_model = self.env['stock.production.lot'] + self.stock_location_model = self.env['stock.location'] + # Get required Model data + self.fixed_account = self.env.ref('account.xfa') + self.purchased_stock = self.env.ref('account.stk') + self.debtors_account = self.env.ref('account.a_recv') + self.cash_account = self.env.ref('account.cash') + self.product_uom = self.env.ref('product.product_uom_unit') + self.journal = self.env.ref('account.miscellaneous_journal') + self.company = self.env.ref('base.main_company') + + location = self.stock_location_model.search([('name', '=', 'WH')]) + self.location = self.stock_location_model.search([('location_id', '=', + location.id)]) + + # Create account for Goods Received Not Invoiced + name = 'Goods Received Not Invoiced' + code = 'grni' + acc_type = 'equity' + self.account_grni = self._create_account(acc_type, name, code, + self.company) + # Create account for Cost of Goods Sold + name = 'Cost of Goods Sold' + code = 'cogs' + acc_type = 'expense' + self.account_cogs = self._create_account(acc_type, name, code, + self.company) + + # Create account for Inventory + name = 'Inventory' + code = 'inventory' + acc_type = 'asset' + self.account_inventory = self._create_account(acc_type, name, code, + self.company) + + # Create account for Inventory Revaluation + name = 'Inventory Revaluation' + code = 'revaluation' + acc_type = 'expense' + self.account_revaluation = self._create_account(acc_type, name, code, + self.company) + + # Create product category + self.product_ctg = self._create_product_category() + + # Create a Product with real cost + standard_price = 10.0 + list_price = 20.0 + self.product_real = self._create_product('real', standard_price, + list_price) + # Add default quantity + quantity = 20.00 + self._update_product_qty(self.product_real, self.location, quantity) + + # Create a Product with average cost + standard_price = 10.0 + list_price = 20.0 + self.product_average = self._create_product('average', standard_price, + list_price) + + # Add default quantity + quantity = 20.00 + self._update_product_qty(self.product_average, self.location, quantity) + + def _create_account(self, acc_type, name, code, company): + """Create an account.""" + type_ids = self.acc_type_model.search([('code', '=', acc_type)]) + account = self.account_model.create({ + 'name': name, + 'code': code, + 'type': 'other', + 'user_type': type_ids.ids and type_ids.ids[0], + 'company_id': company.id + }) + return account + + def _create_product_category(self): + product_ctg = self.product_ctg_model.create({ + 'name': 'test_product_ctg', + 'property_stock_valuation_account_id': self.account_inventory.id, + 'property_inventory_revaluation_increase_account_categ': + self.account_revaluation.id, + 'property_inventory_revaluation_decrease_account_categ': + self.account_revaluation.id, + }) + return product_ctg + + def _create_product(self, cost_method, standard_price, list_price): + """Create a Product with inventory valuation set to auto.""" + product = self.product_model.create({ + 'name': 'test_product', + 'categ_id': self.product_ctg.id, + 'type': 'product', + 'standard_price': standard_price, + 'list_price': list_price, + 'valuation': 'real_time', + 'cost_method': cost_method, + 'property_stock_account_input': self.account_grni.id, + 'property_stock_account_output': self.account_cogs.id, + }) + return product + + def _create_inventory_revaluation(self, journal, revaluation_type): + """Create a Inventory Revaluation with revaluation_type set to + price_change to recalculate inventory value according to new price.""" + inventory = self.reval_model.create({ + 'name': 'test_inventory_revaluation', + 'document_date': datetime.today(), + 'revaluation_type': revaluation_type, + 'journal_id': journal.id, + }) + return inventory + + def _create_inventory_revaluation_line(self, revaluation, product): + """Create a Inventory Revaluation line by applying + increase and decrease account to it.""" + self.increase_account_id = product.categ_id and \ + product.categ_id.\ + property_inventory_revaluation_increase_account_categ + self.decrease_account_id = product.categ_id and \ + product.categ_id.\ + property_inventory_revaluation_decrease_account_categ + + line = self.reval_line_model.create({ + 'product_template_id': product.id, + 'revaluation_id': revaluation.id, + 'increase_account_id': self.increase_account_id.id, + 'decrease_account_id': self.decrease_account_id.id, + }) + return line + + def _update_product_qty(self, product, location, quantity): + """Update Product quantity.""" + product_qty = self.stock_change_model.create({ + 'location_id': location.id, + 'product_id': product.id, + 'new_quantity': quantity, + }) + product_qty.change_product_qty() + return product_qty + + def _get_quant(self, date_from, line): + """Get Quants for Inventory Revaluation between the date supplied.""" + quant = self.get_quant_model.create({ + 'date_from': date_from, + 'date_to': datetime.today(), + }) + line_context = { + 'active_id': line.id, + 'active_ids': line.ids, + 'active_model': 'stock.inventory.revaluation.line', + } + quant.with_context(line_context).process() + for line_quant in line.line_quant_ids: + line_quant.new_cost = 8.0 + + def test_inventory_revaluation_price_change(self): + """Test that the inventory is revaluated when the + inventory price for any product is changed.""" + + # Create an Inventory Revaluation + revaluation_type = 'price_change' + invent_price_change = self._create_inventory_revaluation( + self.journal, revaluation_type) + + # Create an Inventory Revaluation Line for real cost product + invent_line_real = \ + self._create_inventory_revaluation_line( + invent_price_change, self.product_real.product_tmpl_id) + + # Create an Inventory Revaluation Line Quant + date_from = date.today() - timedelta(1) + self._get_quant(date_from, invent_line_real) + + # Create an Inventory Revaluation Line for average cost product + invent_line_avg = self._create_inventory_revaluation_line( + invent_price_change, self.product_average.product_tmpl_id) + # Post the inventory revaluation + invent_line_avg.new_cost = 8.00 + + invent_price_change.button_post() + + expected_result = (10.00 - 8.00) * 20.00 + for line in invent_price_change.line_ids: + for move_line in line.move_id.line_id: + if move_line.debit: + self.assertEqual(move_line.debit, expected_result, + 'Incorrect inventory revaluation for ' + 'type Price Change.') + + def test_inventory_revaluation_value_change(self): + """Test that the inventory is revaluated when the + inventory price for any product is changed.""" + # Create an Inventory Revaluation for value change + revaluation_type = 'inventory_value' + invent_inventory_value = self._create_inventory_revaluation( + self.journal, revaluation_type) + + # Create an Inventory Revaluation Line for average cost product + invent_line_average = self._create_inventory_revaluation_line( + invent_inventory_value, self.product_average.product_tmpl_id) + invent_line_average.new_value = 100.00 + + # Post the inventory revaluation + invent_inventory_value.button_post() + + for line in invent_inventory_value.line_ids: + for move_line in line.move_id.line_id: + if move_line.debit: + self.assertEqual(move_line.debit, 100.0, + 'Incorrect inventory revaluation for ' + 'type Inventory Debit/Credit.') diff --git a/stock_inventory_revaluation/views/product_view.xml b/stock_inventory_revaluation/views/product_view.xml new file mode 100644 index 000000000..7f85728d7 --- /dev/null +++ b/stock_inventory_revaluation/views/product_view.xml @@ -0,0 +1,19 @@ + + + + + + product.category.stock.property.form.inherit + product.category + + + + + + + + + + + diff --git a/stock_inventory_revaluation/views/stock_inventory_revaluation_view.xml b/stock_inventory_revaluation/views/stock_inventory_revaluation_view.xml new file mode 100644 index 000000000..c1b50c5ed --- /dev/null +++ b/stock_inventory_revaluation/views/stock_inventory_revaluation_view.xml @@ -0,0 +1,282 @@ + + + + + + stock.inventory.revaluation.form + stock.inventory.revaluation + +
+
+
+ +
+
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +