# -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. import time from datetime import datetime from flectra.tests.common import TransactionCase from flectra.addons.account.tests.account_test_classes import AccountingTestCase from flectra.tools import DEFAULT_SERVER_DATETIME_FORMAT class TestStockValuation(TransactionCase): def setUp(self): super(TestStockValuation, self).setUp() self.supplier_location = self.env.ref('stock.stock_location_suppliers') self.stock_location = self.env.ref('stock.stock_location_stock') self.partner_id = self.env.ref('base.res_partner_1') self.product1 = self.env.ref('product.product_product_8') Account = self.env['account.account'] self.stock_input_account = Account.create({ 'name': 'Stock Input', 'code': 'StockIn', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.stock_output_account = Account.create({ 'name': 'Stock Output', 'code': 'StockOut', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.stock_valuation_account = Account.create({ 'name': 'Stock Valuation', 'code': 'Stock Valuation', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.stock_journal = self.env['account.journal'].create({ 'name': 'Stock Journal', 'code': 'STJTEST', 'type': 'general', }) self.product1.categ_id.write({ 'property_stock_account_input_categ_id': self.stock_input_account.id, 'property_stock_account_output_categ_id': self.stock_output_account.id, 'property_stock_valuation_account_id': self.stock_valuation_account.id, 'property_stock_journal': self.stock_journal.id, }) def test_change_unit_cost_average_1(self): """ Confirm a purchase order and create the associated receipt, change the unit cost of the purchase order before validating the receipt, the value of the received goods should be set according to the last unit cost. """ self.product1.product_tmpl_id.cost_method = 'average' po1 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 100.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po1.button_confirm() picking1 = po1.picking_ids[0] move1 = picking1.move_lines[0] # the unit price of the purchase order line is copied to the in move self.assertEquals(move1.price_unit, 100) # update the unit price on the purchase order line po1.order_line.price_unit = 200 # the unit price on the stock move is not directly updated self.assertEquals(move1.price_unit, 100) # validate the receipt res_dict = picking1.button_validate() wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) wizard.process() # the unit price of the stock move has been updated to the latest value self.assertEquals(move1.price_unit, 200) self.assertEquals(self.product1.stock_value, 2000) def test_standard_price_change_1(self): """ Confirm a purchase order and create the associated receipt, change the unit cost of the purchase order and the standard price of the product before validating the receipt, the value of the received goods should be set according to the last standard price. """ self.product1.product_tmpl_id.cost_method = 'standard' # set a standard price self.product1.product_tmpl_id.standard_price = 10 po1 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 11.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po1.button_confirm() picking1 = po1.picking_ids[0] move1 = picking1.move_lines[0] # the move's unit price reflects the purchase order line's cost even if it's useless when # the product's cost method is standard self.assertEquals(move1.price_unit, 11) # set a new standard price self.product1.product_tmpl_id.standard_price = 12 # the unit price on the stock move is not directly updated self.assertEquals(move1.price_unit, 11) # validate the receipt res_dict = picking1.button_validate() wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) wizard.process() # the unit price of the stock move has been updated to the latest value self.assertEquals(move1.price_unit, 12) self.assertEquals(self.product1.stock_value, 120) def test_change_currency_rate_average_1(self): """ Confirm a purchase order in another currency and create the associated receipt, change the currency rate, validate the receipt and then check that the value of the received goods is set according to the last currency rate. """ usd_currency = self.env.ref('base.USD') self.env.user.company_id.currency_id = usd_currency.id eur_currency = self.env.ref('base.EUR') self.product1.product_tmpl_id.cost_method = 'average' # default currency is USD, create a purchase order in EUR po1 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'currency_id': eur_currency.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 100.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po1.button_confirm() picking1 = po1.picking_ids[0] move1 = picking1.move_lines[0] # convert the price unit in the company currency price_unit_usd = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=True) # the unit price of the move is the unit price of the purchase order line converted in # the company's currency self.assertAlmostEqual(move1.price_unit, price_unit_usd) # change the rate of the currency self.env['res.currency.rate'].create({ 'name': time.strftime('%Y-%m-%d'), 'rate': 2.0, 'currency_id': eur_currency.id, 'company_id': po1.company_id.id, }) eur_currency._compute_current_rate() price_unit_usd_new_rate = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=False) # the new price_unit is lower than th initial because of the rate's change self.assertLess(price_unit_usd_new_rate, price_unit_usd) # the unit price on the stock move is not directly updated self.assertAlmostEqual(move1.price_unit, price_unit_usd) # validate the receipt res_dict = picking1.button_validate() wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) wizard.process() # the unit price of the stock move has been updated to the latest value self.assertAlmostEqual(move1.price_unit, price_unit_usd_new_rate) self.assertAlmostEqual(self.product1.stock_value, price_unit_usd_new_rate * 10, delta=0.1) def test_extra_move_fifo_1(self): """ Check that the extra move when over processing a receipt is correctly merged back in the original move. """ self.product1.product_tmpl_id.cost_method = 'fifo' po1 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 100.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po1.button_confirm() picking1 = po1.picking_ids[0] move1 = picking1.move_lines[0] move1.quantity_done = 15 res_dict = picking1.button_validate() self.assertEqual(res_dict['res_model'], 'stock.overprocessed.transfer') wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) wizard.action_confirm() # there should be only one move self.assertEqual(len(picking1.move_lines), 1) self.assertEqual(move1.price_unit, 100) self.assertEqual(move1.product_qty, 15) self.assertEqual(self.product1.stock_value, 1500) def test_backorder_fifo_1(self): """ Check that the backordered move when under processing a receipt correctly keep the price unit of the original move. """ self.product1.product_tmpl_id.cost_method = 'fifo' po1 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 100.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po1.button_confirm() picking1 = po1.picking_ids[0] move1 = picking1.move_lines[0] move1.quantity_done = 5 res_dict = picking1.button_validate() self.assertEqual(res_dict['res_model'], 'stock.backorder.confirmation') wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id')) wizard.process() self.assertEqual(len(picking1.move_lines), 1) self.assertEqual(move1.price_unit, 100) self.assertEqual(move1.product_qty, 5) picking2 = po1.picking_ids.filtered(lambda p: p.backorder_id) move2 = picking2.move_lines[0] self.assertEqual(len(picking2.move_lines), 1) self.assertEqual(move2.price_unit, 100) self.assertEqual(move2.product_qty, 5) class TestStockValuationWithCOA(AccountingTestCase): def setUp(self): super(TestStockValuationWithCOA, self).setUp() self.supplier_location = self.env.ref('stock.stock_location_suppliers') self.stock_location = self.env.ref('stock.stock_location_stock') self.partner_id = self.env.ref('base.res_partner_1') self.product1 = self.env.ref('product.product_product_8') Account = self.env['account.account'] self.stock_input_account = Account.create({ 'name': 'Stock Input', 'code': 'StockIn', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.stock_output_account = Account.create({ 'name': 'Stock Output', 'code': 'StockOut', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.stock_valuation_account = Account.create({ 'name': 'Stock Valuation', 'code': 'Stock Valuation', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.stock_journal = self.env['account.journal'].create({ 'name': 'Stock Journal', 'code': 'STJTEST', 'type': 'general', }) self.product1.categ_id.write({ 'property_stock_account_input_categ_id': self.stock_input_account.id, 'property_stock_account_output_categ_id': self.stock_output_account.id, 'property_stock_valuation_account_id': self.stock_valuation_account.id, 'property_stock_journal': self.stock_journal.id, }) def test_fifo_anglosaxon_return(self): self.env.user.company_id.anglo_saxon_accounting = True self.product1.product_tmpl_id.cost_method = 'fifo' self.product1.product_tmpl_id.valuation = 'real_time' self.product1.product_tmpl_id.invoice_policy = 'delivery' price_diff_account = self.env['account.account'].create({ 'name': 'price diff account', 'code': 'price diff account', 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, }) self.product1.property_account_creditor_price_difference = price_diff_account # Receive 10@10 ; create the vendor bill po1 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 10.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po1.button_confirm() receipt_po1 = po1.picking_ids[0] receipt_po1.move_lines.quantity_done = 10 receipt_po1.button_validate() invoice_po1 = self.env['account.invoice'].create({ 'partner_id': self.partner_id.id, 'purchase_id': po1.id, 'account_id': self.partner_id.property_account_payable_id.id, 'type': 'in_invoice', }) invoice_po1.purchase_order_change() invoice_po1.action_invoice_open() # Receive 10@20 ; create the vendor bill po2 = self.env['purchase.order'].create({ 'partner_id': self.partner_id.id, 'order_line': [ (0, 0, { 'name': self.product1.name, 'product_id': self.product1.id, 'product_qty': 10.0, 'product_uom': self.product1.uom_po_id.id, 'price_unit': 20.0, 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT), }), ], }) po2.button_confirm() receipt_po2 = po2.picking_ids[0] receipt_po2.move_lines.quantity_done = 10 receipt_po2.button_validate() invoice_po2 = self.env['account.invoice'].create({ 'partner_id': self.partner_id.id, 'purchase_id': po2.id, 'account_id': self.partner_id.property_account_payable_id.id, 'type': 'in_invoice', }) invoice_po2.purchase_order_change() invoice_po2.action_invoice_open() # valuation of product1 should be 300 self.assertEqual(self.product1.stock_value, 300) # return the second po stock_return_picking = self.env['stock.return.picking']\ .with_context(active_ids=receipt_po2.ids, active_id=receipt_po2.ids[0])\ .create({}) stock_return_picking.product_return_moves.quantity = 10 stock_return_picking_action = stock_return_picking.create_returns() return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) return_pick.move_lines[0].move_line_ids[0].qty_done = 10 return_pick.do_transfer() # valuation of product1 should be 200 as the first items will be sent out self.assertEqual(self.product1.stock_value, 200) # create a credit note for po2 creditnote_po2 = self.env['account.invoice'].create({ 'partner_id': self.partner_id.id, 'purchase_id': po2.id, 'account_id': self.partner_id.property_account_payable_id.id, 'type': 'in_refund', }) creditnote_po2.purchase_order_change() creditnote_po2.invoice_line_ids[0].quantity = 10 creditnote_po2.action_invoice_open() # check the anglo saxon entries price_diff_entry = self.env['account.move.line'].search([('account_id', '=', price_diff_account.id)]) self.assertEqual(price_diff_entry.credit, 100)